Plugin Directory

Changeset 3738687


Ignore:
Timestamp:
10/10/2026 08:35:50 PM (7 days ago)
Author:
dshanske
Message:

Update to version 4.0.0 from GitHub

Location:
indieweb-post-kinds
Files:
50 added
52 deleted
132 edited
1 copied

Legend:

Unmodified
Added
Removed
  • indieweb-post-kinds/tags/4.0.0/.gitattributes

    r2491232 r3738687  
    55/tests export-ignore
    66/bin export-ignore
    7 /node-modules export-ignore
     7/node_modules export-ignore
    88/sass export-ignore
    99
    … …  
    1717/readme.md export-ignore
    1818/phpcs.xml export-ignore
     19/phpcs-templates.xml export-ignore
    1920/phpunit.xml export-ignore
    20 /Gruntfile.js export-ignore
    21 /gruntlint.js export-ignore
     21/eslint.config.js export-ignore
  • indieweb-post-kinds/tags/4.0.0/css/kind.admin.min.css

    r2572705 r3738687  
    1 .svg-icon svg{fill:currentColor;padding:2px;vertical-align:middle;top:-0.0625em}#kind-all svg{width:1rem;height:1rem;color:gray}#kind-menu svg,#kind-posts svg,#tempus-onthisday svg,#tempus-thisweek svg,.kind-titles svg{width:1.5rem;height:1.5rem;color:#fff;mix-blend-mode:difference}#kind-menu ul,#kind-posts ul,#tempus-onthisday ul,#tempus-thisweek ul,.kind-titles ul{list-style-type:none}.response svg{width:2rem;height:2rem;color:#fff;mix-blend-mode:difference}.response figure{margin:auto}#replybox-meta.is-loading .loading{visibility:visible}#replybox-meta label{display:block}#replybox-meta .field-row{overflow:auto;height:100%}#replybox-meta .half{float:left;width:46%;margin:0 1%}#replybox-meta .half:first-of-type{margin-left:0;width:48%}#replybox-meta .half:last-of-type{margin-right:0;width:49%}#replybox-meta .quarter{float:left;margin-left:2%;width:12%}#replybox-meta .eighth{float:left;margin-left:2%;margin-right:1%;width:6%}#replybox-meta input[type=number]{width:100%}#replybox-meta .three-quarters{float:left;width:85%}#replybox-meta .is-loading .loading{visibility:visible}#replybox-meta .loading{visibility:hidden;position:absolute;top:0;bottom:0;left:0;right:0;z-index:10;background-color:#000;background-color:rgba(0,0,0,.4)}#replybox-meta .loading-spinner{margin:0 auto;display:block;position:relative;top:2em}/*# sourceMappingURL=kind.admin.min.css.map */
     1:where(.svg-icon){display:inline-block;width:1em;height:1em;margin-right:.5rem;vertical-align:middle}:where(.svg-icon>span,.svg-icon svg){display:block;width:100%;height:100%}:where(.svg-icon svg){fill:currentColor}:where(#kind-all .svg-icon){width:1rem;height:1rem;color:gray}:where(#kind-menu,#kind-posts,#tempus-onthisday,#tempus-thisweek,.kind-titles) :where(.svg-icon){width:1.5rem;height:1.5rem;color:#fff;mix-blend-mode:difference}#kind-menu ul,#kind-posts ul,#tempus-onthisday ul,#tempus-thisweek ul,.kind-titles ul{list-style-type:none}:where(.response) :where(.svg-icon){width:2rem;height:2rem;color:#fff;mix-blend-mode:difference}.response figure{margin:auto}#replybox-meta.is-loading .loading{visibility:visible}#replybox-meta label{display:block}#replybox-meta .field-row{overflow:auto;height:100%}#replybox-meta .half{float:left;width:46%;margin:0 1%}#replybox-meta .half:first-of-type{margin-left:0;width:48%}#replybox-meta .half:last-of-type{margin-right:0;width:49%}#replybox-meta .quarter{float:left;margin-left:2%;width:12%}#replybox-meta .eighth{float:left;margin-left:2%;margin-right:1%;width:6%}#replybox-meta input[type=number]{width:100%}#replybox-meta .three-quarters{float:left;width:85%}#replybox-meta .is-loading .loading{visibility:visible}#replybox-meta .loading{visibility:hidden;position:absolute;top:0;bottom:0;left:0;right:0;z-index:10;background-color:#000;background-color:rgba(0,0,0,.4)}#replybox-meta .loading-spinner{margin:0 auto;display:block;position:relative;top:2em}/*# sourceMappingURL=kind.admin.min.css.map */
  • indieweb-post-kinds/tags/4.0.0/css/kind.admin.min.css.map

    r2572705 r3738687  
    1 {"version":3,"sourceRoot":"","sources":["../sass/_kinds.scss","../sass/_meta-box.scss"],"names":[],"mappings":"AAAA,cACC,kBACA,YACA,sBACA,cAIA,cACC,WACA,YACA,WAKD,2FACC,aACA,cACA,WACA,0BAED,sFACC,qBAKM,cACQ,WACA,YACd,WACc,0BAEf,iBACC,YCnCF,mCACC,mBAIA,qBACC,cAED,0BACC,cACA,YAED,qBACC,WACA,UACA,YAED,mCACC,cACA,UAED,kCACC,eACA,UAED,wBACC,WACA,eACA,UAED,uBACC,WACA,eACA,gBACA,SAED,kCACC,WAED,+BACC,WACA,UAED,oCACC,mBAED,wBACC,kBACA,kBACA,MACA,SACA,OACA,QACA,WACA,sBACA,gCAED,gCACC,cACA,cACA,kBACA","file":"kind.admin.min.css"}
     1{"version":3,"sourceRoot":"..\/","sources":["sass\/_kinds.scss","sass\/_meta-box.scss"],"names":[],"mappings":"AAGA,kBACC,qBACA,UACA,WACA,mBACA,sBAGD,qCACC,cACA,WACA,YAGD,sBACC,kBAGD,4BACC,WACA,YACA,WAGD,iGACC,aACA,cACA,WACA,0BAIA,sFACC,qBAIF,oCACC,WACA,YACA,WACA,0BAIA,iBACC,YCjDF,mCACC,mBAIA,qBACC,cAED,0BACC,cACA,YAED,qBACC,WACA,UACA,YAED,mCACC,cACA,UAED,kCACC,eACA,UAED,wBACC,WACA,eACA,UAED,uBACC,WACA,eACA,gBACA,SAED,kCACC,WAED,+BACC,WACA,UAED,oCACC,mBAED,wBACC,kBACA,kBACA,MACA,SACA,OACA,QACA,WACA,sBACA,gCAED,gCACC,cACA,cACA,kBACA","file":"kind.admin.min.css"}
  • indieweb-post-kinds/tags/4.0.0/css/kind.min.css

    r2588189 r3738687  
    1 .svg-icon svg{fill:currentColor;padding:2px;vertical-align:middle;top:-0.0625em}#kind-all svg{width:1rem;height:1rem;color:gray}#kind-menu svg,#kind-posts svg,#tempus-onthisday svg,#tempus-thisweek svg,.kind-titles svg{width:1.5rem;height:1.5rem;color:#fff;mix-blend-mode:difference}#kind-menu ul,#kind-posts ul,#tempus-onthisday ul,#tempus-thisweek ul,.kind-titles ul{list-style-type:none}.response svg{width:2rem;height:2rem;color:#fff;mix-blend-mode:difference}.response figure{margin:auto}.response{margin-bottom:10px;margin-top:10px;margin-left:20px;padding:5px;font-size:16px;border-bottom:inset #d6d6d6}.response .kind-icon{margin-right:3px}.response blockquote{margin-left:25px}.response a:link{text-decoration:none}.response a:hover{text-decoration:none}.response img{padding:0;margin:2px}ul.cites{list-style:none;display:inline}.kind-embeds{text-align:center;position:relative;width:100%;margin-top:5px}.kind-photo-thumbnail{margin-bottom:5px}/*# sourceMappingURL=kind.min.css.map */
     1:where(.svg-icon){display:inline-block;width:1em;height:1em;margin-right:.5rem;vertical-align:middle}:where(.svg-icon>span,.svg-icon svg){display:block;width:100%;height:100%}:where(.svg-icon svg){fill:currentColor}:where(#kind-all .svg-icon){width:1rem;height:1rem;color:gray}:where(#kind-menu,#kind-posts,#tempus-onthisday,#tempus-thisweek,.kind-titles) :where(.svg-icon){width:1.5rem;height:1.5rem;color:#fff;mix-blend-mode:difference}#kind-menu ul,#kind-posts ul,#tempus-onthisday ul,#tempus-thisweek ul,.kind-titles ul{list-style-type:none}:where(.response) :where(.svg-icon){width:2rem;height:2rem;color:#fff;mix-blend-mode:difference}.response figure{margin:auto}.response{margin-bottom:10px;margin-top:10px;margin-left:20px;padding:5px;font-size:16px;border-bottom:inset #d6d6d6}.response .kind-icon{margin-right:3px}.response blockquote{margin-left:25px}.response a:link{text-decoration:none}.response a:hover{text-decoration:none}.response img{padding:0;margin:2px}ul.cites{list-style:none;display:inline}.kind-embeds{text-align:center;position:relative;width:100%;margin-top:5px}.kind-photo-thumbnail{margin-bottom:5px}/*# sourceMappingURL=kind.min.css.map */
  • indieweb-post-kinds/tags/4.0.0/css/kind.min.css.map

    r2588189 r3738687  
    1 {"version":3,"sourceRoot":"","sources":["../sass/_kinds.scss","../sass/_frontend.scss"],"names":[],"mappings":"AAAA,cACC,kBACA,YACA,sBACA,cAIA,cACC,WACA,YACA,WAKD,2FACC,aACA,cACA,WACA,0BAED,sFACC,qBAKM,cACQ,WACA,YACd,WACc,0BAEf,iBACC,YCnCF,UACI,mBACA,gBACA,iBACA,YACA,eACA,4BACF,qBACC,iBAED,qBACC,iBAGF,iBACC,qBAGD,kBACC,qBAGD,cACE,UACA,WAIH,SACI,gBACA,eAGJ,aACI,kBACA,kBACA,WACA,eAGJ,sBACI","file":"kind.min.css"}
     1{"version":3,"sourceRoot":"..\/","sources":["sass\/_kinds.scss","sass\/_frontend.scss"],"names":[],"mappings":"AAGA,kBACC,qBACA,UACA,WACA,mBACA,sBAGD,qCACC,cACA,WACA,YAGD,sBACC,kBAGD,4BACC,WACA,YACA,WAGD,iGACC,aACA,cACA,WACA,0BAIA,sFACC,qBAIF,oCACC,WACA,YACA,WACA,0BAIA,iBACC,YCjDF,UACI,mBACA,gBACA,iBACA,YACA,eACA,4BACF,qBACC,iBAED,qBACC,iBAGF,iBACC,qBAGD,kBACC,qBAGD,cACE,UACA,WAIH,SACI,gBACA,eAGJ,aACI,kBACA,kBACA,WACA,eAGJ,sBACI","file":"kind.min.css"}
  • indieweb-post-kinds/tags/4.0.0/develop.txt

    r1787314 r3738687  
    1 == Functions ==
     1Developer notes for Post Kinds 4.0.0. Every function, method and hook listed here has a docblock in the code with its full parameters and return values.
    22
    3 `get_post_kind_slug($id)` - Return the kind slug for a given post. If `$id` is not specified, use current post.
    4 `get_post_kind($id)` - Return the kind string for a given post. If `$id` is not specified, use current post.
     3== Template functions ==
    54
    6 `has_post_kind($kind, $post)` - Returns true/false if kind is in post. If post is empty, then use current post
     5These global functions are loaded on `plugins_loaded`, from `includes/kind-functions.php`.
    76
    8 `get_kind_context_class( $class, $classtype )` - Returns the CSS class to be applied to the response/context if the kind is one for which there is context. Classtype defaults to u, other option is usually p. Sets the class to the kind slug and for specially specified slugs, sets appropriate mf2 classes as well. $class specifies any additional classes to be added.
     7`get_post_kind_slug( $post = null )` - Returns the kind slug of a post, or false if it has no kind. Defaults to the current post.
     8`get_post_kind( $post = null )` - Returns the translated kind name of a post, or false if it has no kind. Defaults to the current post.
     9`has_post_kind( $kinds = array(), $post = null )` - Returns whether a post has any of the given kinds, or any kind at all if `$kinds` is empty.
     10`set_post_kind( $post, $kind )` - Gives a post a kind. Returns a WP_Error if the post or the kind is not valid.
     11`get_post_kind_slugs()` - Returns the slugs of every registered kind.
     12`get_post_kind_string( $slug )` - Returns the translated name of a kind, or an empty string if it is not registered.
     13`get_post_kind_link( $kind )` - Returns the URL of a kind's archive.
     14`kind_display( $post_id = null )` - Prints the response markup for a post. Use it with the `kind_content_display` filter to place the markup in the theme.
     15`get_kind_view_part( $slug, $name = null, $args = null )` - Returns a rendered view. See Kind_View::get_view_part().
     16`kind_get_the_title( $post = null, $args = array() )` - Returns a post's title, or a generated one (the kind's icon or name and a cited name or excerpt) for a post without one.
     17`kind_get_the_link( $post = null, $cls = null, $date_cls = null )` - Returns a link to a post, using kind_get_the_title(), followed by its date.
     18`kind_flatten_array( $a )` - Removes empty values from an array and unwraps single-item lists, recursively.
     19`kind_src_url_in_content( $content )` - Returns every src attribute value in some markup, or 0 if there are none.
    920
     21== Registering kinds ==
    1022
     23`register_post_kind( $slug, $args )` - Registers a kind. Call it on `init`. `includes/register-kinds.php` registers the built-in kinds the same way, and Kind_Taxonomy::register_post_kind() documents every argument.
     24`set_post_kind_visibility( $slug, $show = true )` - Shows or hides a kind in Settings, including the kinds reserved for future use.
    1125
    12 ==  Filters ==
     26== Time functions ==
    1327
    14 `get_the_kind` - Filter get_the_kinds
     28Also global, from `includes/time-functions.php`.
    1529
    16 `the_kinds` - Filter get_the_kinds_list
     30`kind_safe_datetime( $value, $timezone = null, $immutable = true )` - Since 4.0.0. Creates a date from a value, returning false instead of throwing on invalid input.
     31`kind_safe_interval( $value )` - Since 4.0.0. Creates a DateInterval from an ISO 8601 duration such as `PT3M30S`, returning false instead of throwing on invalid input.
     32`calculate_duration( $start_string, $end_string )` - Returns the DateInterval between two dates, or false.
     33`date_interval_to_iso8601( $interval )` - Converts a DateInterval to an ISO 8601 duration string.
     34`build_iso8601_time( $date, $time, $offset )` - Joins a date, time and offset into an ISO 8601 string.
     35`build_iso8601_duration( $values )` - Builds an ISO 8601 duration from its pieces (Y, M, D, H, I, S).
     36`get_gmt_offsets()` - Returns the UTC offsets in use by any time zone, sorted.
     37`get_default_offset()` - Returns the site's UTC offset, such as `-05:00`.
     38`tz_seconds_to_offset( $seconds )`, `tz_offset_to_seconds( $offset )` - Convert between a UTC offset in seconds and an offset string.
     39`tz_seconds_to_timezone( $seconds )`, `tz_timezone_to_seconds( $timezone )` - Convert between a UTC offset in seconds and a DateTimeZone.
    1740
    18 `kind_classes` - Filter get_kind_class
     41== Classes ==
    1942
    20 `kind_verb` - Filter get_kind_verbs
     43`Kind_Post` reads and writes a post's response data. Create one with a post ID or WP_Post. Its main methods:
    2144
    22 `kind-response-display` - Filters the output being added to the_content or to custom location in theme
     45* `get_kind()`, `get_cite( $key = null )`, `get_author()`, `get_photo()`, `get_audio()`, `get_video()`, `get_duration()`, `get_datetime_property( $property )` and `get( $key )` read data.
     46* `set( $key, $value )`, `set_duration( $value )`, `set_datetime_property( $key, $value )` and `delete( $key )` write it. Response data is stored as post meta prefixed `mf2_`.
     47* `get_cite_type()` - Since 4.0.0. Returns the jf2 type of the post's citation: `card` for a checkin, `food` for eat and drink, and `cite` for every other kind.
     48* `Kind_Post::get_attachment_id( $url )` - Since 4.0.0. Static. A cached wrapper for attachment_url_to_postid().
     49
     50`Kind_Time` - Since 4.0.0. Static helpers for the editor's date, time and duration fields: `display_formatted_datetime()`, `divide_datetime()`, `build_datetime()`, `get_datetime_offset()`, `divide_interval()` and `build_interval()`.
     51
     52`Kind_View` builds the response markup. `Kind_View::get_display( $post_id = null )` returns it for a post, `Kind_View::get_view_part( $slug, $name, $args = null )` renders one view, and `Kind_View::get_hcard( $author, $args = null )` returns an author's h-card. Since 4.0.0, its helpers return escaped markup; the 4.0.0 changelog lists the ones that changed.
     53
     54`Kind_Media_Metadata::has_media( $post )` - Since 4.0.0. Returns whether a post may show audio or video. The media fragment script loads only for these posts.
     55
     56`Kind_Upgrade` - Since 4.0.0. Upgrades stored post data in the background, in batches, after the plugin is updated. It gives published posts without a kind one, moves citations out of older storage locations, repairs citations that earlier versions damaged, and records the media in each post.
     57
     58== WP-CLI ==
     59
     60`wp post-kinds upgrade` - Since 4.0.0. Runs the upgrade now instead of in the background. It is safe to run more than once.
     61
     62* `--dry-run` - Report what would change without saving.
     63* `--post=<id>` - Upgrade only this post.
     64
     65== REST API ==
     66
     67* The `kind` field on posts reads and sets a post's kind slug.
     68* `GET /wp-json/post-kinds/1.0/fields?kind={slug}` returns a kind's registration, including its field schema. An unknown kind returns a 404. It requires the `edit_posts` capability.
     69* The editor fetches details for a cited URL from the bundled Parse This library's `parse-this/1.0/parse` route.
     70
     71== Filters ==
     72
     73Display:
     74
     75`kind_content_display` - Whether to add response markup to post content, excerpts and feeds automatically. Return false to place it with kind_display() instead. Default true.
     76`kind_response_display` - The response markup for a post. Receives the markup and the post ID.
     77`kind_view_paths` - The folders searched for views before the plugin's `views` folder. Default the theme's `kind_views` folder.
     78`kind_icon_display` - Whether to show the kind's icon or name before the response. Return false to hide it and remove the option from Settings.
     79`post_kinds_icon_attributes` - The HTML attributes on the span that wraps a kind icon. Output as-is, so it must be escaped. Default empty since 4.0.0; before that, an inline style.
     80`kind_get_the_title_content` - The text of a generated title. Receives escaped text and the post.
     81`kind_get_the_title_before` - The markup before a generated title, the kind's icon or name by default. Output as-is, so it must be escaped.
     82`get_hcard_data` - The author data before it becomes an h-card.
     83`get_hcard` - The h-card markup for an author. Return non-empty, escaped markup to replace the plugin's own.
     84`post_kind_embed_approvelist` - The domains whose cited URLs are embedded with oEmbed.
     85`kind_post_type_string` - The phrase for what a known site's URL points to, such as "a video" for YouTube.
     86
     87Kinds and archives:
     88
     89`kind_include` - The kinds offered in the editor and listed by the Kind Menu widget. Default the kinds selected in Settings, plus note, reply and article.
     90`kind_base_slug` - Since 4.0.0. The start of kind archive URLs, as in `/kind/like/`. Default the Kind base on Settings > Permalinks, or `kind`.
     91`kind_rewrite_taxonomies` - The taxonomies that get `/kind/{kind}/{taxonomy}/{term}/` archives. Default post_tag, category and series.
     92`kind_exclude_slug` - The slug for archives that exclude kinds, as in `/exclude/kind/note,like/`. Default `exclude`.
     93`kind_photos_slug` - The slug for the archive of posts with photos. Default `photos`.
     94`kind_firehose_slug` - The slug for the feed that includes every kind. Default `firehose`.
     95
     96After changing a slug or the rewrite taxonomies, flush rewrite rules: visit Settings > Permalinks and save.
     97
     98Upgrade:
     99
     100`kind_upgrade_batch_size` - Since 4.0.0. The number of posts the background upgrade processes per batch. Default 50.
     101
     102Micropub (these run only when the Micropub plugin is active):
     103
     104`post_kinds_micropub_enrich` - Since 4.0.0. Whether to fill in citation URLs from Micropub requests with data parsed from the cited page. Return false to store citations exactly as the client sent them. Default true.
     105`post_kinds_micropub_parse_timeout` - Since 4.0.0. The timeout, in seconds, for parsing a citation while the client waits. Citations that time out are retried in the background. Default 5.
     106`post_kinds_micropub_unavailable_retry_delay` - Since 4.0.0. How long to wait before retrying, once, a citation whose page returned 404, 401 or 403. Default 6 hours.
     107`post_kinds_micropub_post_types` - Since 4.0.0. The post types advertised to Micropub clients in `q=config`.
     108
     109== Actions ==
     110
     111`change_kind` - Fires when a post's kind changes. Receives the post ID and the old and new kind slugs; either slug can be an empty string.
     112`post_kinds_citation_enrichment` - Since 4.0.0. Fires after each attempt to parse a Micropub citation. Receives the log entry and the post ID.
     113
     114== Deprecated ==
     115
     116These still work, but report a deprecation notice when WP_DEBUG is on.
     117
     118* Since 4.0.0, the global functions `display_formatted_datetime()`, `divide_datetime()`, `build_datetime()`, `get_datetime_offset()`, `divide_interval()` and `build_interval()`. Use the Kind_Time methods of the same names. They will be removed in 5.0.0.
     119* Since 4.0.0, the `MF2_Post` class. Use Kind_Post. It will be removed in a future major version.
     120
     121== Styling icons ==
     122
     123Kind icons are `<span class="svg-icon svg-{kind}">` elements wrapping an SVG. The plugin's stylesheet sizes the span, and the SVG fills it. Its rules use `:where()`, so they have no specificity and any theme rule wins. For example, `.svg-icon { width: 1rem; height: 1rem; }` resizes every icon, and `#kind-menu .svg-icon` only those in the Kind Menu widget. Icons take their colour from the `color` property.
     124
     125== Overriding views ==
     126
     127The markup added to each post comes from a view file in the plugin's `views` folder: `kind-{slug}.php` for a kind (for example `kind-like.php`), or `kind.php` for a kind without its own view.
     128
     129To change a view, copy it into a `kind_views` folder in your theme and edit the copy. If a child theme has a `kind_views` folder, it is used instead of the parent theme's; the two aren't combined. For each post, the plugin looks for `kind-{slug}.php` in your theme and then in the plugin, and only then for `kind.php` in the same order. So a theme `kind.php` replaces only the kinds that have no view of their own. The `kind_view_paths` filter changes which folders are searched before the plugin's.
     130
     131Copies don't receive updates. After upgrading, compare them with the plugin's views. Since 4.0.0, views escape everything they print, and the `Kind_View` helpers return escaped markup.
     132
     133Each view is rendered by `Kind_View::get_view_part()` with these variables in scope:
     134
     135* `$kind_post` (Kind_Post) - the post being displayed.
     136* `$kind` (string) - the kind's slug, such as `like`.
     137* `$type` (string) - the kind's microformats property, such as `like-of`.
     138* `$cite` (array) - the citation, normalized by `Kind_Post::normalize_cite()`, so every key is present. `name` falls back to the URL.
     139* `$url` (string) - the cited URL.
     140* `$author` (string|false) - the cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none. `$cite['authors']` lists each author as a card with `name`, `url` and `photo`.
     141* `$embed` (string) - embed markup for the cited URL, or an empty string.
     142* `$photos` (int[]|false) - attachment IDs of the post's photos, from `Kind_Post::get_photo()`.
     143* `$args` (array) - the arguments passed to `Kind_View::get_view_part()`; `post_id` is the post's ID.
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-config.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Post Kind Configuration Class
     3 * Kind_Config class.
    44 *
    5  * @package Post Kinds
    6  * Sets Up Configuration Options for the Plugin.
     5 * @package Post_Kinds
     6 * @since   2.0.2
    77 */
    88
     9defined( 'ABSPATH' ) || exit;
     10
    911/**
    10  * Static Class to Configure Admin Options.
     12 * Registers the plugin settings and builds the settings page.
    1113 *
    12  * @package Post Kinds
     14 * @since 2.0.2
    1315 */
    1416class Kind_Config {
    … …  
    1618        /**
    1719         * Function to Initialize the Configuration.
     20         *
     21         * @since 2.0.2
    1822         *
    1923         * @access public
    … …  
    2226                add_action( 'admin_init', array( static::class, 'admin_init' ) );
    2327                add_action( 'admin_menu', array( static::class, 'admin_menu' ), 11 );
    24                 // Add post help tab
     28                // Add post help tab.
    2529                add_action( 'load-post.php', array( static::class, 'add_post_help_tab' ), 20 );
    26 
    27                 $args = array(
    28                         'type'         => 'array',
    29                         'description'  => 'Kinds Enabled on This Site',
    30                         'show_in_rest' => false,
    31                         'default'      => array( 'article', 'reply', 'bookmark' ),
     30                add_action( 'load-options-permalink.php', array( static::class, 'save_kind_base' ) );
     31
     32                $args = array(
     33                        'type'              => 'array',
     34                        'description'       => 'Kinds Enabled on This Site',
     35                        'show_in_rest'      => false,
     36                        'default'           => array( 'article', 'reply', 'bookmark' ),
     37                        'sanitize_callback' => array( static::class, 'sanitize_kind_list' ),
    3238                );
    3339                register_setting( 'iwt_options', 'kind_termslist', $args );
    3440                $args = array(
    35                         'type'         => 'string',
    36                         'description'  => 'Default Kind',
    37                         'show_in_rest' => false,
    38                         'default'      => 'note',
     41                        'type'              => 'string',
     42                        'description'       => 'Default Kind',
     43                        'show_in_rest'      => false,
     44                        'default'           => 'note',
     45                        'sanitize_callback' => array( static::class, 'sanitize_default_kind' ),
    3946                );
    4047                register_setting( 'iwt_options', 'kind_default', $args );
    4148                $args = array(
    42                         'type'         => 'boolean',
    43                         'description'  => 'Rich Embed Support for Whitelisted Sites',
    44                         'show_in_rest' => false,
    45                         'default'      => 1,
     49                        'type'              => 'boolean',
     50                        'description'       => 'Rich Embed Support for Whitelisted Sites',
     51                        'show_in_rest'      => false,
     52                        'default'           => 1,
     53                        'sanitize_callback' => array( static::class, 'sanitize_checkbox' ),
    4654                );
    4755                register_setting( 'iwt_options', 'kind_embeds', $args );
    4856                $args = array(
    49                         'type'         => 'array',
    50                         'description'  => 'Kinds Showing in Main Archive',
    51                         'show_in_rest' => false,
    52                         'default'      => array(),
     57                        'type'              => 'array',
     58                        'description'       => 'Kinds Showing in Main Archive',
     59                        'show_in_rest'      => false,
     60                        'default'           => array(),
     61                        'sanitize_callback' => array( static::class, 'sanitize_kind_list' ),
    5362                );
    5463                register_setting( 'iwt_options', 'kind_firehose', $args );
    5564
    5665                $args = array(
    57                         'type'         => 'boolean',
    58                         'description'  => 'Response Information Should Be After Content',
    59                         'show_in_rest' => false,
    60                         'default'      => 0,
     66                        'type'              => 'boolean',
     67                        'description'       => 'Response Information Should Be After Content',
     68                        'show_in_rest'      => false,
     69                        'default'           => 0,
     70                        'sanitize_callback' => array( static::class, 'sanitize_checkbox' ),
    6171                );
    6272                register_setting( 'iwt_options', 'kind_bottom', $args );
    6373                $args = array(
    64                         'type'         => 'string',
    65                         'description'  => 'Display Preferences for Before Kind',
    66                         'show_in_rest' => false,
    67                         'default'      => 'icon',
     74                        'type'              => 'string',
     75                        'description'       => 'Display Preferences for Before Kind',
     76                        'show_in_rest'      => false,
     77                        'default'           => 'icon',
     78                        'sanitize_callback' => array( static::class, 'sanitize_display' ),
    6879                );
    6980                register_setting( 'iwt_options', 'kind_display', $args );
    7081                $args = array(
    71                         'type'         => 'string',
    72                         'description'  => 'KSES Content Protection on Responses',
    73                         'show_in_rest' => false,
    74                         'default'      => str_replace( '},"', "},\r\n\"", wp_json_encode( wp_kses_allowed_html( 'post' ), 128 ) ),
     82                        'type'              => 'string',
     83                        'description'       => 'KSES Content Protection on Responses',
     84                        'show_in_rest'      => false,
     85                        'default'           => str_replace( '},"', "},\r\n\"", wp_json_encode( wp_kses_allowed_html( 'post' ), 128 ) ),
     86                        'sanitize_callback' => array( static::class, 'sanitize_kses' ),
    7587                );
    7688                register_setting( 'iwt_options', 'kind_kses', $args );
    7789                $args = array(
    78                         'type'         => 'boolean',
    79                         'description'  => 'Automatically add the Kind to the Title',
    80                         'show_in_rest' => false,
    81                         'default'      => 0,
     90                        'type'              => 'boolean',
     91                        'description'       => 'Automatically add the Kind to the Title',
     92                        'show_in_rest'      => false,
     93                        'default'           => 0,
     94                        'sanitize_callback' => array( static::class, 'sanitize_checkbox' ),
    8295                );
    8396                register_setting( 'iwt_options', 'kind_title', $args );
     97                $args = array(
     98                        'type'              => 'string',
     99                        'description'       => 'Kind Base',
     100                        'show_in_rest'      => false,
     101                        'default'           => '',
     102                        'sanitize_callback' => array( static::class, 'sanitize_kind_base' ),
     103                );
     104                register_setting( 'permalink', 'kind_base', $args );
     105        }
     106
     107        /**
     108         * Sanitizes the kind_base setting.
     109         *
     110         * @since 4.0.0
     111         *
     112         * @param mixed $value Submitted value.
     113         * @return string The base, such as 'type' or 'blog/type', or an empty string for the default.
     114         */
     115        public static function sanitize_kind_base( $value ) {
     116                if ( ! is_string( $value ) ) {
     117                        return '';
     118                }
     119                $segments = array_filter( array_map( 'sanitize_title', explode( '/', $value ) ) );
     120                $base     = implode( '/', $segments );
     121                return 'kind' === $base ? '' : $base;
     122        }
     123
     124        /**
     125         * Saves the Kind base field on Settings > Permalinks.
     126         *
     127         * Core's permalink page doesn't save fields that plugins add, so this runs
     128         * before it, with its nonce. Core flushes the rewrite rules when the page
     129         * reloads.
     130         *
     131         * @since 4.0.0
     132         */
     133        public static function save_kind_base() {
     134                if ( ! isset( $_POST['kind_base'] ) || ! is_string( $_POST['kind_base'] ) || ! current_user_can( 'manage_options' ) ) {
     135                        return;
     136                }
     137                check_admin_referer( 'update-permalink' );
     138                // Sanitized by sanitize_kind_base(), the setting's sanitize callback.
     139                update_option( 'kind_base', wp_unslash( $_POST['kind_base'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
     140        }
     141
     142        /**
     143         * Outputs the Kind base field on Settings > Permalinks.
     144         *
     145         * @since 4.0.0
     146         */
     147        public static function kind_base_callback() {
     148                printf(
     149                        '<input name="kind_base" id="kind_base" type="text" value="%1$s" class="regular-text code" placeholder="kind" />',
     150                        esc_attr( get_option( 'kind_base', '' ) )
     151                );
     152        }
     153
     154        /**
     155         * Sanitizes a list of kinds: the kind_termslist and kind_firehose settings.
     156         *
     157         * @since 4.0.0
     158         *
     159         * @param mixed $value Submitted value.
     160         * @return string[] Registered kind slugs.
     161         */
     162        public static function sanitize_kind_list( $value ) {
     163                if ( ! is_array( $value ) ) {
     164                        return array();
     165                }
     166                $value = array_map( 'sanitize_key', array_filter( $value, 'is_string' ) );
     167                return array_values( array_unique( array_intersect( $value, Kind_Taxonomy::get_kind_list() ) ) );
     168        }
     169
     170        /**
     171         * Sanitizes the default kind setting.
     172         *
     173         * @since 4.0.0
     174         *
     175         * @param mixed $value Submitted value.
     176         * @return string A registered kind slug, or note.
     177         */
     178        public static function sanitize_default_kind( $value ) {
     179                $value = is_string( $value ) ? sanitize_key( $value ) : '';
     180                return in_array( $value, Kind_Taxonomy::get_kind_list(), true ) ? $value : 'note';
     181        }
     182
     183        /**
     184         * Sanitizes the display setting for the text before a kind.
     185         *
     186         * @since 4.0.0
     187         *
     188         * @param mixed $value Submitted value.
     189         * @return string icon, text, both or hide. Defaults to icon.
     190         */
     191        public static function sanitize_display( $value ) {
     192                return in_array( $value, array( 'icon', 'text', 'both', 'hide' ), true ) ? $value : 'icon';
     193        }
     194
     195        /**
     196         * Sanitizes a checkbox setting.
     197         *
     198         * @since 4.0.0
     199         *
     200         * @param mixed $value Submitted value.
     201         * @return int 1 if checked, otherwise 0.
     202         */
     203        public static function sanitize_checkbox( $value ) {
     204                return empty( $value ) ? 0 : 1;
     205        }
     206
     207        /**
     208         * Sanitizes the allowed HTML setting, a JSON object as wp_kses() expects.
     209         *
     210         * Invalid JSON is rejected and the previous value kept.
     211         *
     212         * @since 4.0.0
     213         *
     214         * @param mixed $value Submitted value.
     215         * @return string JSON-encoded allowed HTML.
     216         */
     217        public static function sanitize_kses( $value ) {
     218                $decoded = is_string( $value ) ? json_decode( $value, true ) : null;
     219                if ( is_array( $decoded ) ) {
     220                        return $value;
     221                }
     222                // The settings error API is only loaded in the admin.
     223                if ( function_exists( 'add_settings_error' ) ) {
     224                        add_settings_error( 'kind_kses', 'kind_kses_invalid', __( 'The allowed HTML must be a valid JSON object. The previous value has been kept.', 'indieweb-post-kinds' ) );
     225                }
     226                $previous = get_option( 'kind_kses' );
     227                return is_string( $previous ) ? $previous : '';
     228        }
     229
     230        /**
     231         * Returns the kinds enabled in settings (the kind_termslist option).
     232         *
     233         * Always returns an array of registered kind slugs, whatever is stored: the
     234         * option can be missing, false or hold kinds that are no longer registered.
     235         * Note, which is always available, is not included unless it was saved.
     236         *
     237         * @since 4.0.0
     238         *
     239         * @return string[] Kind slugs.
     240         */
     241        public static function get_termslist() {
     242                $termslist = get_option( 'kind_termslist' );
     243                if ( ! is_array( $termslist ) ) {
     244                        return array();
     245                }
     246                return array_values( array_intersect( $termslist, Kind_Taxonomy::get_kind_list() ) );
    84247        }
    85248
    86249        /**
    87250         * Function to Set up Settings.
     251         *
     252         * @since 2.0.2
    88253         *
    89254         * @access public
    … …  
    92257                add_action( 'admin_bar_menu', array( static::class, 'dashbar_links' ), 20 );
    93258                add_action( 'admin_bar_menu', array( static::class, 'remove_dashbar_post' ), 200 );
     259                add_settings_field(
     260                        'kind_base',
     261                        __( 'Kind base', 'indieweb-post-kinds' ),
     262                        array( static::class, 'kind_base_callback' ),
     263                        'permalink',
     264                        'optional',
     265                        array( 'label_for' => 'kind_base' )
     266                );
    94267                add_settings_section(
    95268                        'iwt-content',
    … …  
    189362                        )
    190363                );
    191                 // Add Query Var to Admin
     364                // Add Query Var to Admin.
    192365                add_filter( 'query_vars', array( static::class, 'query_var' ) );
    193366        }
    … …  
    195368        /**
    196369         * Function to add our kindurl query var.
     370         *
     371         * @since 2.3.3
    197372         *
    198373         * @access public
    … …  
    209384         * Function to remove "post" from the "New" admin bar section.
    210385         *
    211          * @access public
    212          *
    213          * @param WP_Admin_Bar $wp_admin_bar
     386         * @since 3.0.7
     387         *
     388         * @access public
     389         *
     390         * @param WP_Admin_Bar $wp_admin_bar The admin bar.
    214391         */
    215392        public static function remove_dashbar_post( $wp_admin_bar ) {
    … …  
    220397         * Function to add our Post Kind post links to the "New" admin bar section.
    221398         *
    222          * @access public
    223          * @param WP_Admin_Bar $wp_admin_bar
     399         * @since 3.0.7
     400         *
     401         * @access public
     402         *
     403         * @param WP_Admin_Bar $wp_admin_bar The admin bar.
    224404         */
    225405        public static function dashbar_links( $wp_admin_bar ) {
    226                 $termslist = get_option( 'kind_termslist' );
    227                 // Note can never be removed
     406                $termslist = self::get_termslist();
     407                // Note can never be removed.
    228408                array_unshift( $termslist, 'note' );
    229                 foreach ( $termslist as $term ) {
     409                foreach ( array_unique( $termslist ) as $term ) {
    230410                        $wp_admin_bar->add_menu(
    231411                                array(
    … …  
    242422         * Adds Options Page for Plugin Options.
    243423         *
     424         * @since 2.0.2
     425         *
    244426         * @access public
    245427         */
    … …  
    249431                        add_submenu_page(
    250432                                'indieweb',
    251                                 __( 'Post Kinds', 'indieweb-post-kinds' ), // page title
    252                                 __( 'Post Kinds', 'indieweb-post-kinds' ), // menu title
    253                                 'manage_options', // access capability
     433                                __( 'Post Kinds', 'indieweb-post-kinds' ), // page title.
     434                                __( 'Post Kinds', 'indieweb-post-kinds' ), // menu title.
     435                                'manage_options', // access capability.
    254436                                'kind_options',
    255437                                array( static::class, 'options_form' )
    … …  
    263445         * Callback for Options on Options Page.
    264446         *
     447         * @since 2.0.2
     448         *
    265449         * @access public
    266450         */
    … …  
    270454        /**
    271455         * Generate a Checkbox.
     456         *
     457         * @since 2.0.2
    272458         *
    273459         * @access public
    … …  
    280466                $option  = get_option( $args['name'] );
    281467                $checked = intval( $option ?? 0 );
    282                 printf( '<input name="%1$s" type="hidden" value="0" />', esc_attr( $args['name'] ) ); // phpcs:ignore
    283                 printf( '<input name="%1$s" type="checkbox" value="1" %2$s />', esc_attr( $args['name'] ), checked( 1, $checked, false) ); // phpcs:ignore
     468                printf( '<input name="%1$s" type="hidden" value="0" />', esc_attr( $args['name'] ) );
     469                printf( '<input name="%1$s" type="checkbox" value="1" %2$s />', esc_attr( $args['name'] ), checked( 1, $checked, false ) );
    284470        }
    285471
    286472        /**
    287473         * Generate a Textbox.
     474         *
     475         * @since 2.0.2
    288476         *
    289477         * @access public
    … …  
    296484                $option = get_option( $args['name'] );
    297485                if ( is_array( $option ) ) {
    298                         $option = print_r( $option, true );
    299                 }
    300                 echo "<textarea rows='10' cols='50' class='large-text code' name='" . esc_attr( $args['name'] ) . "'>" . $option . '</textarea> '; // phpcs:ignore
     486                        $option = wp_json_encode( $option, JSON_PRETTY_PRINT );
     487                }
     488                echo "<textarea rows='10' cols='50' class='large-text code' name='" . esc_attr( $args['name'] ) . "'>" . esc_textarea( (string) $option ) . '</textarea> ';
    301489        }
    302490
    303491        /**
    304492         * Generate a Term List.
     493         *
     494         * @since 2.5.0
    305495         *
    306496         * @access public
    … …  
    309499                $terms = Kind_Taxonomy::get_kind_list();
    310500                // Hide these terms until ready for use for now.
    311                 $termslist = get_option( 'kind_termslist' );
     501                $termslist = self::get_termslist();
    312502                echo '<div id="kind-all">';
    313503                foreach ( $terms as $term ) {
    314504                        $value = Kind_Taxonomy::get_post_kind_info( $term );
    315505                        if ( $value->show ) {
    316                                 printf( '<input name="kind_termslist[]" type="checkbox" value="%1$s" %2$s />', esc_attr( $term ), checked( in_array( $term, $termslist, true ), true, false ) ); // phpcs:ignore
    317                                 printf( '%1$s<strong>%2$s</strong> - %3$s<br />', Kind_Taxonomy::get_icon( $term ), sanitize_text_field( $value->singular_name ), sanitize_text_field( $value->description ) );  // phpcs:ignore
     506                                printf( '<input name="kind_termslist[]" type="checkbox" value="%1$s" %2$s />', esc_attr( $term ), checked( in_array( $term, $termslist, true ), true, false ) );
     507                                printf( '%1$s<strong>%2$s</strong> - %3$s<br />', Kind_Taxonomy::get_icon( $term ), esc_html( $value->singular_name ), esc_html( $value->description ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon markup.
    318508                        }
    319509                }
    … …  
    322512
    323513        /**
    324          * Generate a Term List.
    325          *
    326          * @access public
     514         * Outputs a multiple select of kinds for a setting.
     515         *
     516         * @since 3.6.0
     517         *
     518         * @access public
     519         *
     520         * @param array $args Field arguments. 'name' is the option name.
    327521         */
    328522        public static function kindmultiselect_callback( array $args ) {
    329                 $terms   = get_option( 'kind_termslist' );
     523                $terms   = self::get_termslist();
    330524                $terms[] = 'note';
     525                $terms   = array_unique( $terms );
    331526
    332527                $kindlist = get_option( $args['name'] );
    … …  
    339534                foreach ( $terms as $term ) {
    340535                        $value = Kind_Taxonomy::get_post_kind_info( $term );
    341                         printf( '<option value="%1$s" %2$s />%3$s</option>', esc_attr($term), selected( in_array( $term, $kindlist ), true, false ), sanitize_text_field( $value->singular_name ) );  // phpcs:ignore
     536                        printf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $term ), selected( in_array( $term, $kindlist, true ), true, false ), esc_html( $value->singular_name ) );
    342537                }
    343538                echo '</select>';
    … …  
    349544         * Generate a Kind Select List.
    350545         *
     546         * @since 2.3.3
     547         *
    351548         * @access public
    352549         */
    353550        public static function defaultkind_callback() {
    354                 $terms   = get_option( 'kind_termslist' );
     551                $terms   = self::get_termslist();
    355552                $terms[] = 'note';
     553                $terms   = array_unique( $terms );
    356554                sort( $terms, SORT_STRING );
    357555
    … …  
    361559                foreach ( $terms as $term ) {
    362560                        $value = Kind_Taxonomy::get_post_kind_info( $term );
    363                         printf( '<option value="%1$s" %2$s />%3$s</option>', esc_attr($term), selected( $term, $defaultkind, false ), sanitize_text_field( $value->singular_name ) );  // phpcs:ignore
     561                        printf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $term ), selected( $term, $defaultkind, false ), esc_html( $value->singular_name ) );
    364562                }
    365563                echo '</select>';
    … …  
    368566        /**
    369567         * Generate Radio Options.
     568         *
     569         * @since 2.6.6
    370570         *
    371571         * @access public
    … …  
    376576                $display = get_option( 'kind_display' );
    377577                foreach ( $args['options'] as $key => $value ) {
    378                         printf( '<input id="%1$s" name="%1$s" type="radio" value="%2$s" class="%3$s" %4$s />%5$s<br />', esc_attr( $args['name'] ), esc_attr( $key ), esc_attr( $args['class'] ), checked( $key, $display, false ), sanitize_text_field( $value ) ); // phpcs:ignore
     578                        printf( '<input id="%1$s" name="%1$s" type="radio" value="%2$s" class="%3$s" %4$s />%5$s<br />', esc_attr( $args['name'] ), esc_attr( $key ), esc_attr( $args['class'] ), checked( $key, $display, false ), esc_html( $value ) );
    379579                }
    380580        }
    … …  
    382582        /**
    383583         * Generate Options Form.
     584         *
     585         * @since 2.0.2
    384586         *
    385587         * @access public
    … …  
    406608        /**
    407609         * Function to generate a help tab in the WordPress admin help tab.
     610         *
     611         * @since 2.3.0
    408612         *
    409613         * @access public
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-fields.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Kind Fields Class
     3 * Kind_Fields class.
    44 *
    5  * @package Post Kind
    6  * Used to Generate Form Fields for a Post UI
     5 * @package Post_Kinds
     6 * @since   3.3.4
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Generates form fields for the posting UI from a kind's property schema.
    713 *
    8  * Fields currently supported and their options{
    9         * datetime
    10         * duration
    11         * author - is a representation of an author and consists of name, url, and photo by default
    12         * select
    13                 * options - associative array of values for the select. Key being the value and the value being the description
    14         * url
    15         * text
    16         * list
    17         * textarea
    18         * number
    19         * cite - is a representation of an h-cite object
    20 * }
     14 * Supported field types and their options:
     15 * - datetime
     16 * - duration
     17 * - author: a name, URL and photo by default.
     18 * - select: `options` is an associative array of value => label.
     19 * - url
     20 * - text
     21 * - list
     22 * - textarea
     23 * - number
     24 * - cite: an h-cite object.
     25 *
     26 * @since 3.3.4
    2127 */
    22 
    2328class Kind_Fields {
    2429
     30        /**
     31         * Returns a list of time zones with translated labels.
     32         *
     33         * @since 3.3.4
     34         *
     35         * @return string[] Labels, keyed by time zone identifier.
     36         */
    2537        public static function timezone_list() {
    2638                return array(
    … …  
    279291        }
    280292
     293        /**
     294         * Returns the UTC offsets in use by any time zone, sorted.
     295         *
     296         * @since 3.3.4
     297         *
     298         * @return string[] Offsets such as '-05:00'.
     299         */
    281300        public static function get_offset_list() {
    282301                $o = wp_cache_get( 'kind_offset_list' );
    … …  
    288307                foreach ( $t_zones as $a ) {
    289308                        $datetime = new DateTime( 'now', new DateTimeZone( $a ) );
    290                         $o[]      = get_datetime_offset( $datetime );
     309                        $o[]      = Kind_Time::get_datetime_offset( $datetime );
    291310                }
    292311                $o = array_unique( $o );
    … …  
    297316
    298317        /**
    299          * Function to render date/time field inputs.
    300          *
    301          * @access public
    302          *
    303          * @param string|DateTime $time   Date/time value.
    304          * @param string $name Property name
    305          * @param array $args
    306          * @param string|array $class  Class to use for fields.
    307          * @return string
     318         * Renders date, time and offset inputs.
     319         *
     320         * @since 3.3.4
     321         *
     322         * @param array                 $args     Field arguments, as returned by validate(): 'name', 'label' and 'class'.
     323         * @param DateTime|string|false $datetime Date, or a date string. Default now.
     324         * @return string The inputs.
    308325         */
    309326        public static function field_datetime( $args, $datetime ) {
    … …  
    315332                }
    316333                $return   = array();
    317                 $time     = divide_datetime( $datetime );
     334                $time     = Kind_Time::divide_datetime( $datetime );
    318335                $return[] = sprintf( '<label for="mf2_%1$s" class="%2$s">%3$s', $args['name'], $args['class'], $args['label'] );
    319336                $return[] = sprintf( '<input type="date" name="mf2_%1$s_date" id="mf2_%1$s_date" value="%2$s"/>', $args['name'], ( $time['date'] ?? '' ) );
    … …  
    328345
    329346        /**
    330          * Function to render dateinterval field inputs.
    331          *
    332          * @access public
    333          *
    334          * @param string|Kind_DateTime $time   Date/time value.
    335          * @param string $name Property name
    336          * @param array $args
    337          * @param string|array $class  Class to use for fields.
    338          * @return string
     347         * Renders duration inputs.
     348         *
     349         * @since 3.3.4
     350         *
     351         * @param array               $args     Field arguments, as returned by validate(), including 'pieces'.
     352         * @param DateInterval|string $interval Duration, or an ISO 8601 duration. Default 0.
     353         * @return string The inputs.
    339354         */
    340355        public static function field_duration( $args, $interval ) {
    … …  
    346361                }
    347362                $return   = array();
    348                 $duration = divide_interval( $interval );
     363                $duration = Kind_Time::divide_interval( $interval );
    349364                $max      = array(
    350365                        'Y' => 1000,
    … …  
    364379
    365380        /**
    366          * Function to render author inputs.
    367          *
    368          * @access public
    369          *
    370          * @param string $prefix Field prefix.
    371          * @param string|array $author Defautl values. If string, considered to be the name property below. {
    372                  * @param string $name Author Name
    373                  * @param string $url Author URL
    374                  * @param string $photo Author Photo
    375          * }
    376          * @return string
     381         * Renders author name, URL and photo inputs.
     382         *
     383         * @since 3.3.4
     384         *
     385         * @param array       $args   Field arguments, as returned by validate().
     386         * @param array|false $author Values, with 'name', 'url' and 'photo' keys.
     387         * @return string The inputs.
    377388         */
    378389        public static function field_author( $args, $author ) {
    … …  
    394405                        ),
    395406                );
    396                 // Ensure all props are set for values
     407                // Ensure all props are set for values.
    397408                foreach ( array_keys( $props ) as $prop ) {
    398409                        if ( ! array_key_exists( $prop, $author ) ) {
    … …  
    405416
    406417        /**
    407          * Function to render a select.
    408          *
    409          * @access public
    410          *
    411          * @param string $name
    412          * @param string $selected Selected field type.
    413          * @return string
     418         * Renders a select. The options are in $args['options'].
     419         *
     420         * @since 3.3.4
     421         *
     422         * @param array  $args     Field arguments, as returned by validate().
     423         * @param string $selected Value of the selected option.
     424         * @return string The input.
    414425         */
    415426        public static function field_select( $args, $selected ) {
    … …  
    426437
    427438        /**
    428          * Function to render a url.
    429          *
    430          * @access public
    431          *
    432          * @param string $name
    433          * @param string $url
    434          * @return string
     439         * Renders a URL input.
     440         *
     441         * @since 3.3.4
     442         *
     443         * @param array  $args Field arguments, as returned by validate().
     444         * @param string $url  Value.
     445         * @return string The input.
    435446         */
    436447        public static function field_url( $args, $url ) {
    … …  
    443454
    444455        /**
    445          * Function to render a number.
    446          *
    447          * @access public
    448          *
    449          * @param string $name
    450          * @param string $value
    451          * @return string
     456         * Renders a number input. The step is in $args['step'].
     457         *
     458         * @since 3.3.4
     459         *
     460         * @param array  $args  Field arguments, as returned by validate().
     461         * @param string $value Value.
     462         * @return string The input.
    452463         */
    453464        public static function field_number( $args, $value ) {
    … …  
    460471
    461472        /**
    462          * Function to render a text.
    463          *
    464          * @access public
    465          *
    466          * @param string $name
    467          * @param string $value
    468          * @return string
     473         * Renders a text input.
     474         *
     475         * @since 3.3.4
     476         *
     477         * @param array  $args  Field arguments, as returned by validate().
     478         * @param string $value Value.
     479         * @return string The input.
    469480         */
    470481        public static function field_text( $args, $value ) {
    … …  
    477488
    478489        /**
    479          * Function to render a textarea.
    480          *
    481          * @access public
    482          *
    483          * @param string $name
    484          * @param string $value
    485          * @return string
     490         * Renders a textarea.
     491         *
     492         * @since 3.3.4
     493         *
     494         * @param array  $args  Field arguments, as returned by validate().
     495         * @param string $value Value.
     496         * @return string The input.
    486497         */
    487498        public static function field_textarea( $args, $value ) {
    … …  
    493504        }
    494505
     506        /**
     507         * Renders a textarea for a list, with items separated by semicolons.
     508         *
     509         * @since 3.3.4
     510         *
     511         * @param array        $args  Field arguments, as returned by validate().
     512         * @param string|array $value Value, or a list of values.
     513         * @return string The input.
     514         */
    495515        public static function field_list( $args, $value ) {
    496516                if ( is_array( $value ) ) {
    … …  
    500520        }
    501521
     522        /**
     523         * Renders the inputs for a venue's properties.
     524         *
     525         * @since 3.3.4
     526         *
     527         * @param array $args  Field arguments, as returned by validate(), including 'properties'.
     528         * @param array $value Values, keyed by property.
     529         * @return string The inputs.
     530         */
    502531        public static function field_venue( $args, $value ) {
    503532                return self::render( $args['properties'], $value );
    504533        }
    505534
     535        /**
     536         * Renders the inputs for a citation's properties.
     537         *
     538         * @since 3.3.4
     539         *
     540         * @param array $args  Field arguments, as returned by validate(), including 'properties'.
     541         * @param array $value Values, keyed by property.
     542         * @return string The inputs.
     543         */
    506544        public static function field_cite( $args, $value ) {
    507545                return self::render( $args['properties'], $value );
    508546        }
    509547
     548        /**
     549         * Checks whether a string starts with a prefix.
     550         *
     551         * @since 3.3.4
     552         *
     553         * @param string $source The source string.
     554         * @param string $prefix The prefix to look for.
     555         * @return bool True if the source starts with the prefix.
     556         */
    510557        public static function str_prefix( $source, $prefix ) {
    511558                return strncmp( $source, $prefix, strlen( $prefix ) ) === 0;
    512559        }
    513560
     561        /**
     562         * Returns a value from an array.
     563         *
     564         * @since 3.3.4
     565         *
     566         * @param string $key   Key.
     567         * @param array  $array The array.
     568         * @return mixed The value, or false if it is not set.
     569         */
    514570        private static function get( $key, $array ) {
    515571                if ( ! is_array( $array ) ) {
    … …  
    522578        }
    523579
     580        /**
     581         * Checks a field definition and fills in defaults.
     582         *
     583         * @since 3.3.4
     584         *
     585         * @param array $element Field definition.
     586         * @return array|false The field definition, or false if it is not valid.
     587         */
    524588        private static function validate( $element ) {
    525                 // Everything must have a type
     589                // Everything must have a type.
    526590                if ( ! self::get( 'type', $element ) ) {
    527591                        return false;
    528592                }
    529                 // Everything must have a label
     593                // Everything must have a label.
    530594                if ( ! self::get( 'label', $element ) ) {
    531595                        return false;
    532596                }
    533                 // If no name property then the label property copies over
     597                // If no name property then the label property copies over.
    534598                if ( ! self::get( 'name', $element ) ) {
    535599                        $element['name'] = sanitize_title( $element['label'] );
    536600                }
    537                 // If no label property then the name property copies over
     601                // If no label property then the name property copies over.
    538602                if ( ! self::get( 'label', $element ) ) {
    539603                        $element['label'] = $element['name'];
    540604                }
    541                 // If no class property then the class property is set to empty
     605                // If no class property then the class property is set to empty.
    542606                if ( ! self::get( 'class', $element ) ) {
    543607                        $element['class'] = '';
    544608                }
    545                 // Class can be an array
     609                // Class can be an array.
    546610                if ( is_array( $element['class'] ) ) {
    547611                        $element['class'] = implode( ' ', $element['class'] );
    548612                }
    549613                $type = self::get( 'type', $element );
    550                 // Any non supported type should be considered to be text
     614                // Any non supported type should be considered to be text.
    551615                if ( ! self::supported_type( $type ) ) {
    552616                        $type = 'text';
    553617                }
    554                 // Type Specific Conditions
     618                // Type Specific Conditions.
    555619                switch ( $type ) {
    556620                        case 'cite':
    … …  
    562626                        case 'number':
    563627                                if ( ! array_key_exists( 'step', $element ) ) {
    564                                         $element['step'] = 1; // Defaults to Even Numbers
     628                                        $element['step'] = 1; // Defaults to Even Numbers.
    565629                                }
    566630                                break;
    567631                        case 'select':
    568                                 if ( ! array_key_exists( 'options', $elements ) ) {
     632                                if ( ! array_key_exists( 'options', $element ) ) {
    569633                                        return false;
    570634                                }
    571635                                break;
    572636                        case 'duration':
    573                                 if ( array_key_exists( 'pieces', $elements ) ) {
    574                                         // Ensure only valid options
    575                                         $element['pieces'] = array_intersect( array( 'Y', 'M', 'D', 'H', 'I', 'S' ), $elements['pieces'] );
     637                                if ( array_key_exists( 'pieces', $element ) ) {
     638                                        // Ensure only valid options.
     639                                        $element['pieces'] = array_intersect( array( 'Y', 'M', 'D', 'H', 'I', 'S' ), $element['pieces'] );
    576640                                } else {
    577                                         // By default only show hours, minutes, seconds
     641                                        // By default only show hours, minutes, seconds.
    578642                                        $element['pieces'] = array( 'H', 'I', 'S' );
    579643                                }
    … …  
    582646                return $element;
    583647        }
     648
     649        /**
     650         * Whether a field type is supported.
     651         *
     652         * @since 3.3.4
     653         *
     654         * @param string $type Field type.
     655         * @return bool
     656         */
    584657        public static function supported_type( $type ) {
    585658                return in_array( $type, array( 'cite', 'venue', 'coordinate', 'author', 'datetime', 'duration', 'text', 'url', 'textarea', 'list', 'section' ), true );
    … …  
    588661
    589662        /**
    590          * Sets an array with only the mf2 prefixed meta.
    591          *
     663         * Returns the properties stored in mf2_ prefixed post meta, without the prefix.
     664         *
     665         * Reads the current post; the $post argument is not used.
     666         *
     667         * @since 3.3.4
     668         *
     669         * @param int|WP_Post $post Not used.
     670         * @return array|false The properties, or false if there is no current post.
    592671         */
    593672        private function get_mf2meta( $post ) {
    … …  
    606685                                unset( $meta[ $key ] );
    607686                                $key = str_replace( 'mf2_', '', $key );
    608                                 // Do not save microput prefixed instructions
     687                                // Do not save microput prefixed instructions.
    609688                                if ( self::str_prefix( $key, 'mp-' ) ) {
    610689                                        continue;
    … …  
    625704
    626705        /**
    627          * Function to render a form from a schema array
    628          *
    629          * @access public
    630          *
    631          * @param array $schema
    632          *
    633          * @return string Form
     706         * Renders a form from a field schema.
     707         *
     708         * @since 3.3.4
     709         *
     710         * @param array $schema Field definitions, keyed by property.
     711         * @param array $values Optional. Values, keyed by property. Default empty array.
     712         * @return string The form.
    634713         */
    635714        public static function render( $schema, $values = array() ) {
    … …  
    649728        }
    650729
    651         /* Extracts microformats elements from post data.
    652          * Microformats elements are prefixed by mf2_
    653          * After that, underscore would indicate properties that need to be reconstituted
    654         */
     730        /**
     731         * Extracts microformats properties from submitted form data.
     732         *
     733         * Properties are prefixed by mf2_. After that, an underscore marks the
     734         * pieces of a property that need to be put back together.
     735         *
     736         * @since 3.3.4
     737         *
     738         * @param array $data Form data, such as $_POST.
     739         * @return array The properties.
     740         */
    655741        public static function rebuild_data( $data ) {
    656742                $raw = array();
    … …  
    669755                                $raw[ $pieces[0] ] = $pieces[1];
    670756                                unset( $raw[ $key ] );
    671                                 // If this has the elements of a duration
     757                                // If this has the elements of a duration.
    672758                                if ( ! empty( array_intersect( array( 'Y', 'M', 'D', 'H', 'I', 'S' ), $raw[ $pieces[0] ] ) ) ) {
    673                                         $interval          = build_interval( $raw[ $pieces[0] ] );
     759                                        $interval          = Kind_Time::build_interval( $raw[ $pieces[0] ] );
    674760                                        $raw[ $pieces[0] ] = date_interval_to_iso8601( $interval );
    675761                                }
    676                                 // If this has the elements of a datetime
     762                                // If this has the elements of a datetime.
    677763                                if ( ! empty( array_intersect( array( 'date', 'time', 'offset' ), $raw[ $pieces[0] ] ) ) ) {
    678                                         $datetime          = build_datetime( $raw[ $pieces[0] ] );
     764                                        $datetime          = Kind_Time::build_datetime( $raw[ $pieces[0] ] );
    679765                                        $raw[ $pieces[0] ] = $datetime->format( DATE_W3C );
    680766                                }
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-media-metadata.php

    r3001401 r3738687  
    11<?php
    22/**
     3 * Kind_Media_Metadata class.
    34 *
    4  * Enhances Metadata for Media
     5 * @package Post_Kinds
     6 * @since   3.1.0
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Enhances the metadata stored for media attachments, and records the media a post uses.
     13 *
     14 * @since 3.1.0
    515 */
    616class Kind_Media_Metadata {
    … …  
    919         * Function to Initialize the Configuration.
    1020         *
     21         * @since 3.1.0
     22         *
    1123         * @access public
    1224         */
    1325        public static function init() {
    14                 $wp_version = get_bloginfo( 'version' );
    1526                add_filter( 'wp_generate_attachment_metadata', array( static::class, 'wp_generate_attachment_metadata' ), 33, 2 );
    16                 if ( version_compare( $wp_version, '5.3', '>' ) ) {
    17                         add_filter( 'wp_update_attachment_metadata', array( static::class, 'wp_sanitize_media_metadata' ), 9, 2 );
    18                 }
     27                add_filter( 'wp_update_attachment_metadata', array( static::class, 'wp_sanitize_media_metadata' ), 9, 2 );
    1928                add_action( 'wp_enqueue_scripts', array( static::class, 'enqueue' ) );
    2029
    21                 add_action( 'save_post', array( static::class, 'save_post' ), 20 );
     30                // Kinds apply to posts only; revisions and autosaves have their own post type.
     31                add_action( 'save_post_post', array( static::class, 'save_post' ), 20 );
     32
     33                // The photo property can change outside a save, for example from Micropub.
     34                foreach ( array( 'added_post_meta', 'updated_post_meta', 'deleted_post_meta' ) as $hook ) {
     35                        add_action( $hook, array( static::class, 'photo_meta_changed' ), 10, 3 );
     36                }
    2237
    2338                add_filter( 'attachment_fields_to_edit', array( static::class, 'attachment_fields_to_edit' ), 10, 2 );
    … …  
    2540        }
    2641
     42        /**
     43         * Whether the request is for an AMP page, according to the AMP plugin.
     44         *
     45         * @since 3.5.1
     46         *
     47         * @return bool
     48         */
    2749        public static function is_amp_endpoint() {
    2850                if ( function_exists( 'is_amp_endpoint' ) ) {
    … …  
    3254        }
    3355
     56        /**
     57         * Enqueues the media fragment script on single posts that have media.
     58         *
     59         * @since 3.3.0
     60         */
    3461        public static function enqueue() {
    35                 if ( ! is_front_page() && is_singular() && ! self::is_amp_endpoint() ) {
    36                         wp_enqueue_script(
    37                                 'media-fragment',
    38                                 plugins_url( 'js/clone-media-fragment.js', __DIR__ ),
    39                                 array(),
    40                                 '1.0',
    41                                 true
    42                         );
    43                 }
     62                if ( is_front_page() || ! is_singular() || self::is_amp_endpoint() || ! self::has_media( get_queried_object() ) ) {
     63                        return;
     64                }
     65                wp_enqueue_script(
     66                        'media-fragment',
     67                        plugins_url( 'js/clone-media-fragment.js', __DIR__ ),
     68                        array(),
     69                        '1.0',
     70                        // WordPress 6.2 reads the array as a true $in_footer; 6.3 and later also defer it.
     71                        array(
     72                                'in_footer' => true,
     73                                'strategy'  => 'defer',
     74                        )
     75                );
     76        }
     77
     78        /**
     79         * Returns whether a post may show audio or video, which the media
     80         * fragment script links to.
     81         *
     82         * @since 4.0.0
     83         *
     84         * @param mixed $post Post object, usually the queried object.
     85         * @return bool True if the post has an audio or video kind or property, or
     86         *              audio or video in its content.
     87         */
     88        public static function has_media( $post ) {
     89                if ( ! $post instanceof WP_Post ) {
     90                        return false;
     91                }
     92                if ( 'post' === $post->post_type ) {
     93                        if ( has_post_kind( array( 'audio', 'video', 'listen', 'watch', 'jam' ), $post ) ) {
     94                                return true;
     95                        }
     96                        if ( get_post_meta( $post->ID, 'mf2_audio', true ) || get_post_meta( $post->ID, 'mf2_video', true ) ) {
     97                                return true;
     98                        }
     99                }
     100                return (bool) preg_match( '/<(audio|video)[\s>]|\[(audio|video|playlist)[\s\]]/i', $post->post_content );
    44101        }
    45102
    … …  
    47104        /**
    48105         * Displays the Media Creator Description on the Attachment Page.
     106         *
     107         * @since 3.4.0
    49108         *
    50109         * @param array   $form_fields See attachment_fields_to_edit filter in WordPress.
    … …  
    55114
    56115                $author = get_post_meta( $post->ID, 'mf2_author', true );
    57                 if ( Parse_This_MF2_Utils::is_microformat( $author ) ) {
    58                         $author = mf2_to_jf2( $author );
     116                if ( \ParseThis\MF2_Utils::is_microformat( $author ) ) {
     117                        $author = \ParseThis\mf2_to_jf2( $author );
    59118                } else {
    60119                        $author = array();
    … …  
    81140         * Saves Extra Fields.
    82141         *
    83          * @param array $post Attachment post data.
    84          * @param array   $attachment data.
     142         * @since 3.4.0
     143         *
     144         * @param array $post       Attachment post data.
     145         * @param array $attachment Attachment fields submitted.
    85146         */
    86147        public static function attachment_fields_to_save( $post, $attachment ) {
     148                // Core checks this before saving; check again as the filter can be applied elsewhere.
     149                if ( empty( $post['ID'] ) || ! current_user_can( 'edit_post', $post['ID'] ) ) {
     150                        return $post;
     151                }
     152                // Core passes the request data slashed.
    87153                $author = array();
    88                 if ( isset( $attachment['author_name'] ) && ! empty( $attachment['author_name'] ) ) {
    89                         $author['name'] = $attachment['author_name'];
    90                 }
    91                 if ( isset( $attachment['author_url'] ) && ! empty( $attachment['author_url'] ) ) {
    92                         $author['url'] = $attachment['author_url'];
     154                if ( isset( $attachment['author_name'] ) && is_string( $attachment['author_name'] ) ) {
     155                        $author['name'] = sanitize_text_field( wp_unslash( $attachment['author_name'] ) );
     156                }
     157                if ( isset( $attachment['author_url'] ) && is_string( $attachment['author_url'] ) ) {
     158                        $author['url'] = sanitize_url( trim( wp_unslash( $attachment['author_url'] ) ), array( 'http', 'https' ) );
    93159                }
    94160                $author = array_filter( $author );
    … …  
    97163                } else {
    98164                        $author['type'] = 'card';
    99                         update_post_meta( $post['ID'], 'mf2_author', jf2_to_mf2( $author ) );
     165                        // The metadata API unslashes, so slash the sanitized values.
     166                        update_post_meta( $post['ID'], 'mf2_author', wp_slash( \ParseThis\jf2_to_mf2( $author ) ) );
    100167                }
    101168                return $post;
    102169        }
    103170
    104         /*
     171        /**
     172         * Finds the media library attachments used in a content block.
     173         *
     174         * The content is parsed once for images, audio and video. An image's ID
     175         * is taken from its wp-image-{id} class when it has one; otherwise, and
     176         * for audio and video sources, the URL is looked up.
     177         *
     178         * @since 4.0.0
     179         *
     180         * @param string $content Content.
     181         * @return array {
     182         *     Attachment IDs found, each list without duplicates or zeros.
     183         *
     184         *     @type int[] $img   Image attachment IDs.
     185         *     @type int[] $audio Audio attachment IDs.
     186         *     @type int[] $video Video attachment IDs.
     187         * }
     188         */
     189        private static function get_media_from_content( $content ) {
     190                $return = array(
     191                        'img'   => array(),
     192                        'audio' => array(),
     193                        'video' => array(),
     194                );
     195                $content = wp_unslash( (string) $content );
     196                // Most content has no media; skip parsing it.
     197                if ( false === stripos( $content, '<img' ) && false === stripos( $content, '<audio' ) && false === stripos( $content, '<video' ) ) {
     198                        return $return;
     199                }
     200                $doc = \ParseThis\pt_load_domdocument( $content );
     201
     202                foreach ( $doc->getElementsByTagName( 'img' ) as $image ) {
     203                        $id = 0;
     204                        foreach ( explode( ' ', $image->getAttribute( 'class' ) ) as $class ) {
     205                                if ( 0 === strpos( $class, 'wp-image-' ) ) {
     206                                        $id = (int) substr( $class, strlen( 'wp-image-' ) );
     207                                        break;
     208                                }
     209                        }
     210                        if ( ! $id ) {
     211                                $id = attachment_url_to_postid( $image->getAttribute( 'src' ) );
     212                        }
     213                        $return['img'][] = $id;
     214                }
     215                foreach ( array( 'audio', 'video' ) as $tag ) {
     216                        foreach ( $doc->getElementsByTagName( $tag ) as $media ) {
     217                                foreach ( $media->getElementsByTagName( 'source' ) as $source ) {
     218                                        $return[ $tag ][] = attachment_url_to_postid( remove_query_arg( '_', $source->getAttribute( 'src' ) ) );
     219                                }
     220                        }
     221                }
     222                foreach ( $return as $type => $ids ) {
     223                        $return[ $type ] = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) );
     224                }
     225                return $return;
     226        }
     227
     228        /**
    105229         * Determine Attached Images from a Content Block.
     230         *
     231         * @since 3.4.0
    106232         *
    107233         * @param string $content Content.
    108234         * @return array Array of Attachment IDs.
    109         */
     235         */
    110236        public static function get_img_from_content( $content ) {
    111                 $content = wp_unslash( $content );
    112                 $return  = array();
    113                 $doc     = pt_load_domdocument( $content );
    114                 $images  = $doc->getElementsByTagName( 'img' );
    115                 foreach ( $images as $image ) {
    116                         $classes = $image->getAttribute( 'class' );
    117                         $classes = explode( ' ', $classes );
    118                         foreach ( $classes as $class ) {
    119                                 if ( 0 === strpos( $class, 'wp-image-' ) ) {
    120                                         $id = (int) str_replace( 'wp-image-', '', $class );
    121                                         if ( 0 !== $id ) {
    122                                                 $return[] = $id;
    123                                         }
    124                                         break;
    125                                 }
    126                         }
    127                         $url = $image->getAttribute( 'src' );
    128                         $id  = attachment_url_to_postid( $url );
    129                         if ( 0 !== $id ) {
    130                                 $return[] = $id;
    131                         }
    132                 }
    133                 return array_unique( $return );
    134         }
    135 
    136         /*
     237                return self::get_media_from_content( $content )['img'];
     238        }
     239
     240        /**
    137241         * Determine Attached Audio from a Content Block.
     242         *
     243         * @since 3.4.0
    138244         *
    139245         * @param string $content Content.
    140246         * @return array Array of Attachment IDs.
    141         */
     247         */
    142248        public static function get_audio_from_content( $content ) {
    143                 $content = wp_unslash( $content );
    144                 $return  = array();
    145                 $doc     = pt_load_domdocument( $content );
    146                 $audios  = $doc->getElementsByTagName( 'audio' );
    147                 foreach ( $audios as $audio ) {
    148                         $sources = $audio->getElementsByTagName( 'source' );
    149                         foreach ( $sources as $source ) {
    150                                 $url      = remove_query_arg( '_', $source->getAttribute( 'src' ) );
    151                                 $id       = attachment_url_to_postid( $url );
    152                                 $return[] = $id;
    153                         }
    154                 }
    155                 return array_unique( $return );
    156         }
    157 
    158         /*
     249                return self::get_media_from_content( $content )['audio'];
     250        }
     251
     252        /**
    159253         * Determine Attached Videos from a Content Block.
     254         *
     255         * @since 3.4.0
    160256         *
    161257         * @param string $content Content.
    162258         * @return array Array of Attachment IDs.
    163         */
     259         */
    164260        public static function get_video_from_content( $content ) {
    165                 $content = wp_unslash( $content );
    166                 $return  = array();
    167                 $doc     = pt_load_domdocument( $content );
    168                 $videos  = $doc->getElementsByTagName( 'video' );
    169                 foreach ( $videos as $video ) {
    170                         $sources = $video->getElementsByTagName( 'source' );
    171                         foreach ( $sources as $source ) {
    172                                 $url      = remove_query_arg( '_', $source->getAttribute( 'src' ) );
    173                                 $id       = attachment_url_to_postid( $url );
    174                                 $return[] = $id;
    175                         }
    176                 }
    177                 return array_unique( $return );
    178         }
    179 
    180         private function media_sideload_image( $url, $post_id, $description = null ) {
    181                 // To prevent sideloading the same URL multiple times check for the original URL which will now be stored
    182                 $ids = get_posts(
    183                         array(
    184                                 'post_type'        => 'attachment',
    185                                 'suppress_filters' => false,
    186                                 'nopaging'         => true,
    187                                 'meta_key'         => '_source_url',
    188                                 'meta_value'       => $url,
    189                                 'fields'           => 'ids',
    190                         )
    191                 );
    192                 if ( ! empty( $ids ) ) {
    193                         return $ids[0];
    194                 }
    195                 $id = media_sideload_image( $url, $post_id, $description, 'id' );
    196                 // Version 5.4 adds this automatically.
    197                 if ( $id && version_compare( $wp_version, '5.3', '>' ) ) {
    198                         update_post_meta( $id, '_source_url', $url );
    199                 }
    200                 return $id;
    201         }
    202 
    203 
    204 
    205         /**
    206          * Every time the post is saved check for media embedded in content and save a list of attachment IDs.
    207         */
     261                return self::get_media_from_content( $content )['video'];
     262        }
     263
     264        /**
     265         * Every time a post is saved, stores the attachment IDs of the media in its content.
     266         *
     267         * The lists are kept in the _content_img_ids, _content_audio_ids and
     268         * _content_video_ids meta; a key is deleted when there is no media of that
     269         * type, so the photos archive can select posts by whether the key exists.
     270         *
     271         * @since 3.4.0
     272         *
     273         * @param int $post_id Post ID.
     274         */
    208275        public static function save_post( $post_id ) {
    209                 $post    = get_post( $post_id );
    210                 $content = do_shortcode( $post->post_content );
    211 
    212                 $gallery_ids = array();
    213                 $gallery     = get_post_gallery( $post_id, false );
    214 
    215                 if ( is_array( $gallery ) ) {
    216                         if ( array_key_exists( 'ids', $gallery ) ) {
    217                                 $gallery_ids = explode( ',', $gallery['ids'] );
    218                         }
    219                 }
    220                 $ids = self::get_img_from_content( $content );
    221                 if ( is_array( $ids ) && is_array( $gallery_ids ) ) {
    222                         $ids = array_merge( $ids, $gallery_ids );
    223                 }
    224 
    225                 if ( ! $ids ) {
    226                         delete_post_meta( $post_id, '_content_img_ids' );
     276                // The classic editor's draft autosave; the next save will record the media.
     277                if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
     278                        return;
     279                }
     280                $post = get_post( $post_id );
     281                if ( ! $post ) {
     282                        return;
     283                }
     284                $content = $post->post_content;
     285                // Shortcodes such as [gallery], [audio] and [video] produce the media markup.
     286                if ( false !== strpos( $content, '[' ) ) {
     287                        $content = do_shortcode( $content );
     288                }
     289                $media = self::get_media_from_content( $content );
     290
     291                $gallery = get_post_gallery( $post_id, false );
     292                if ( is_array( $gallery ) && ! empty( $gallery['ids'] ) ) {
     293                        $media['img'] = array_values( array_unique( array_merge( $media['img'], array_filter( array_map( 'intval', explode( ',', $gallery['ids'] ) ) ) ) ) );
     294                }
     295
     296                foreach ( $media as $type => $ids ) {
     297                        $key = '_content_' . $type . '_ids';
     298                        if ( empty( $ids ) ) {
     299                                delete_post_meta( $post_id, $key );
     300                        } else {
     301                                update_post_meta( $post_id, $key, $ids );
     302                        }
     303                }
     304                self::update_photo_flag( $post_id );
     305        }
     306
     307        /**
     308         * Updates the photo flag when a post's photo property changes.
     309         *
     310         * @since 4.0.0
     311         *
     312         * @param int|int[] $meta_id   Meta ID, or IDs when deleting.
     313         * @param int       $object_id Post ID.
     314         * @param string    $meta_key  Meta key.
     315         */
     316        public static function photo_meta_changed( $meta_id, $object_id, $meta_key ) {
     317                if ( 'mf2_photo' === $meta_key && 'post' === get_post_type( $object_id ) ) {
     318                        self::update_photo_flag( $object_id );
     319                }
     320        }
     321
     322        /**
     323         * Records whether a post has photos, for the photos archive.
     324         *
     325         * A post has photos when it has images in its content or a photo
     326         * property. The _kind_has_photo meta exists only for those posts, so the
     327         * archive can select them with one indexed meta key.
     328         *
     329         * @since 4.0.0
     330         *
     331         * @param int $post_id Post ID.
     332         */
     333        public static function update_photo_flag( $post_id ) {
     334                if ( get_post_meta( $post_id, '_content_img_ids', true ) || get_post_meta( $post_id, 'mf2_photo', true ) ) {
     335                        update_post_meta( $post_id, '_kind_has_photo', 1 );
    227336                } else {
    228                         update_post_meta( $post_id, '_content_img_ids', $ids );
    229                 }
    230                 $ids = self::get_video_from_content( $content );
    231                 if ( ! $ids ) {
    232                         delete_post_meta( $post_id, '_content_video_ids' );
    233                 } else {
    234                         update_post_meta( $post_id, '_content_video_ids', $ids );
    235                 }
    236                 $ids = self::get_audio_from_content( $content );
    237                 if ( ! $ids ) {
    238                         delete_post_meta( $post_id, '_content_audio_ids' );
    239                 } else {
    240                         update_post_meta( $post_id, '_content_audio_ids', $ids );
     337                        delete_post_meta( $post_id, '_kind_has_photo' );
    241338                }
    242339        }
    … …  
    248345         * bad character encodings from causing database calls and API endpoints to fail.
    249346         *
    250          * @param array $metadata An existing array with data
     347         * @since 3.3.0
     348         *
     349         * @param array $metadata An existing array with data.
    251350         *
    252351         * @return array Returns array of sanitized metadata.
    … …  
    271370        }
    272371
     372        /**
     373         * Stores an attachment's author, album and duration from its file metadata.
     374         *
     375         * The image credit, or the artist of audio or video, becomes the author;
     376         * the album becomes the publication.
     377         *
     378         * @since 3.3.4
     379         *
     380         * @param array|mixed $data          Attachment metadata.
     381         * @param int         $attachment_id Attachment ID.
     382         * @return array|mixed The metadata, with empty values removed.
     383         */
    273384        public static function wp_generate_attachment_metadata( $data, $attachment_id ) {
    274385                if ( ! is_array( $data ) ) {
    275386                        return $data;
    276387                }
    277                 $data       = array_filter( $data );
    278                 $attachment = get_post( $attachment_id, ARRAY_A );
     388                $data = array_filter( $data );
    279389                if ( isset( $data['image_meta'] ) ) {
    280390                        $meta = $data['image_meta'];
    … …  
    283393                                        $attachment_id,
    284394                                        'mf2_author',
    285                                         jf2_to_mf2(
     395                                        \ParseThis\jf2_to_mf2(
    286396                                                array(
    287397                                                        'name' => $meta['credit'],
    … …  
    299409                                $attachment_id,
    300410                                'mf2_author',
    301                                 jf2_to_mf2(
     411                                \ParseThis\jf2_to_mf2(
    302412                                        array(
    303413                                                'name' => $data['artist'],
    … …  
    308418                }
    309419                if ( isset( $data['length'] ) ) {
    310                         update_post_meta( $attachment_id, 'mf2_duration', array( seconds_to_iso8601( $data['length'] ) ) );
     420                        update_post_meta( $attachment_id, 'mf2_duration', array( \ParseThis\seconds_to_iso8601( $data['length'] ) ) );
    311421                }
    312422                return $data;
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-media-view.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Kind Media View Class
     3 * Kind_Media_View class.
    44 *
    5  * @package Post Kind
    6  * Used to Display Media
     5 * @package Post_Kinds
     6 * @since   3.3.0
    77 */
    88
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Builds the markup for images, audio and video in kind views.
     13 *
     14 * @since 3.3.0
     15 */
    916class Kind_Media_View {
     17        /**
     18         * Attachment IDs or media URLs.
     19         *
     20         * @since 3.3.0
     21         * @var int[]|string[]|int|string
     22         */
    1023        private $ids;
     24
     25        /**
     26         * Media type: 'photo', 'audio' or 'video'.
     27         *
     28         * @since 3.3.0
     29         * @var string
     30         */
    1131        private $type;
     32
     33        /**
     34         * Sets up the media to display.
     35         *
     36         * @since 3.3.0
     37         *
     38         * @param int[]|string[]|int|string $ids  Attachment IDs or media URLs.
     39         * @param string                    $type Media type: 'photo', 'audio' or 'video'.
     40         */
    1241        public function __construct( $ids, $type ) {
    1342                $this->ids  = $ids;
    … …  
    1544        }
    1645
     46        /**
     47         * Returns the markup for the media.
     48         *
     49         * @since 3.3.0
     50         *
     51         * @return string The markup, or an empty string if the type is not supported.
     52         */
    1753        public function get() {
    1854                switch ( $this->type ) {
    … …  
    2763        }
    2864
     65        /**
     66         * Returns a one-column gallery of photos.
     67         *
     68         * @since 3.3.0
     69         *
     70         * @param int[] $photos Attachment IDs.
     71         * @return string The gallery markup.
     72         */
    2973        private function photo( $photos ) {
    3074                return gallery_shortcode(
    … …  
    4185         * Return a media view for the audio post kind.
    4286         *
     87         * @since 3.3.0
     88         *
    4389         * @access private
    44          * @param int|string   $id   Audio attachment ID or audio URL
    45          * @param mixed $args Arguments for the audio media view.
     90         *
     91         * @param int|string|array $id   Audio attachment ID or audio URL, or a list of them.
     92         * @param array|null       $args Arguments for the audio media view.
    4693         *
    4794         * @return array|string|void
    … …  
    72119                $return = '';
    73120                if ( $args['src'] ) {
     121                        $args   = wp_parse_args( $args, $default );
    74122                        $return = wp_audio_shortcode( $args );
    75123                }
    … …  
    80128         * Return a media view for the video post kind.
    81129         *
     130         * @since 3.3.0
     131         *
    82132         * @access private
    83          * @param int|string   $id   Video attachment ID or URL.
    84          * @param mixed $args Arguments for the video media view.
     133         *
     134         * @param int|string|array $id   Video attachment ID or URL, or a list of them.
     135         * @param array|null       $args Arguments for the video media view.
    85136         *
    86137         * @return array|string|void
    … …  
    98149                } elseif ( wp_http_validate_url( $id ) ) {
    99150                        $args['src'] = $id;
    100                 } if ( 0 === $id ) {
     151                } elseif ( 0 === $id ) {
    101152                        return '';
    102153                } else {
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-menu-widget.php

    r3067251 r3738687  
    11<?php
     2/**
     3 * Kind_Menu_Widget class.
     4 *
     5 * @package Post_Kinds
     6 * @since   3.1.3
     7 */
    28
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Widget that lists links to the archive of each kind.
     13 *
     14 * @since 3.1.3
     15 */
    316class Kind_Menu_Widget extends WP_Widget {
    417        /**
    518         * Register widget with WordPress.
     19         *
     20         * @since 3.1.3
    621         */
    722        public function __construct() {
    823                parent::__construct(
    9                         'Kind_Menu_Widget',                // Base ID
    10                         __( 'Kind Menu Widget', 'indieweb-post-kinds' ),        // Name
     24                        'Kind_Menu_Widget',                // Base ID.
     25                        __( 'Kind Menu Widget', 'indieweb-post-kinds' ),        // Name.
    1126                        array(
    1227                                'classname'             => 'kind_menu_widget',
    … …  
    1934        /**
    2035         * Front-end display of widget.
     36         *
     37         * @since 3.1.3
    2138         *
    2239         * @see WP_Widget::widget()
    … …  
    3552                }
    3653                $include = array_merge( $include, array( 'note', 'reply', 'article' ) );
    37                 // Filter Kinds
     54                /**
     55                 * This filter is documented in includes/class-kind-taxonomy.php
     56                 */
    3857                $include = array_unique( apply_filters( 'kind_include', $include ) );
    39                 // Note cannot be removed or disabled without hacking the code
     58                // Note cannot be removed or disabled without hacking the code.
    4059                if ( ! in_array( 'note', $include, true ) ) {
    4160                        $include[] = 'note';
    4261                }
    4362
    44                 // phpcs:ignore
    45                 echo $args['before_widget'];
     63                echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper markup registered by the theme.
    4664                if ( $title ) {
    47                         echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore
     65                        echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Wrapper markup is the theme's; the title is escaped by core's widget_title filter.
    4866                }
    4967                ?>
    … …  
    5270                <ul>
    5371                <?php
    54                 $feed = is_front_page() ? 'rel="feed"' : '';
     72                $feed = is_front_page() ? ' rel="feed"' : '';
    5573                foreach ( $include as $i ) {
    5674                        $count = Kind_Taxonomy::get_post_kind_count( $i );
    … …  
    6078                        $name = ( 1 === $count ) ? Kind_Taxonomy::get_kind_info( $i, 'singular_name' ) : Kind_Taxonomy::get_kind_info( $i, 'name' );
    6179                        /* translators: 1. Blog Title 2. Kind Singular Name */
    62                         $title = sprintf( __( 'title="%1$s %2$s Feed"', 'indieweb-post-kinds' ), esc_attr( get_bloginfo( 'name' ) ), esc_attR( Kind_Taxonomy::get_kind_info( $i, 'singular_name' ) ) );
    63                         if ( 1 === (int) $instance['count'] ) {
    64                                 $count = sprintf( '%1$s (%2$s)', $name, $count );
     80                        $feed_title = sprintf( __( '%1$s %2$s Feed', 'indieweb-post-kinds' ), get_bloginfo( 'name' ), Kind_Taxonomy::get_kind_info( $i, 'singular_name' ) );
     81                        if ( 1 === (int) ( $instance['count'] ?? 0 ) ) {
     82                                $label = sprintf( '%1$s (%2$s)', $name, number_format_i18n( $count ) );
    6583                        } else {
    66                                 $count = $name;
     84                                $label = $name;
    6785                        }
    6886                        printf(
    69                                 '<li><a %4$s %5$s type="text/html" href="%2$s">%1$s%3$s</a></li>',
    70                                 Kind_Taxonomy::get_icon( $i ), // phpcs:ignore
     87                                '<li><a%4$s title="%5$s" type="text/html" href="%2$s">%1$s%3$s</a></li>',
     88                                Kind_Taxonomy::get_icon( $i ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon markup.
    7189                                esc_url( Kind_Taxonomy::get_post_kind_link( $i ) ),
    72                                 $count, // phpcs:ignore
    73                                 $feed, // phpcs:ignore
    74                                 $title  // phpcs:ignore
    75                         ); // phpcs:ignore
     90                                esc_html( $label ),
     91                                $feed, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- A fixed attribute string.
     92                                esc_attr( $feed_title )
     93                        );
    7694                }
    77                 if ( 1 === (int) $instance['all'] ) {
    78                         printf( '<li><a type="text/html" href="%2$s">%1$s%3$s</a></li>', Kind_Taxonomy::get_icon( 'firehose' ), esc_url( get_post_type_archive_link( 'post' ) ), esc_html__( 'All Posts', 'indieweb-post-kinds' ) ); // phpcs:ignore
     95                if ( 1 === (int) ( $instance['all'] ?? 0 ) ) {
     96                        printf( '<li><a type="text/html" href="%2$s">%1$s%3$s</a></li>', Kind_Taxonomy::get_icon( 'firehose' ), esc_url( get_post_type_archive_link( 'post' ) ), esc_html__( 'All Posts', 'indieweb-post-kinds' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon markup.
    7997                }
    8098                ?>
    … …  
    83101
    84102                <?php
    85                 // phpcs:ignore
    86                 echo $args['after_widget'];
     103                echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper markup registered by the theme.
    87104        }
    88105
    … …  
    90107        /**
    91108         * Sanitize widget form values as they are saved.
     109         *
     110         * @since 3.1.3
    92111         *
    93112         * @see WP_Widget::update()
    … …  
    99118         */
    100119        public function update( $new_instance, $old_instance ) {
    101                 array_walk_recursive( $new_instance, 'sanitize_text_field' );
    102                 return $new_instance;
     120                $termslist = isset( $new_instance['termslist'] ) && is_array( $new_instance['termslist'] ) ? array_filter( $new_instance['termslist'], 'is_string' ) : array();
     121                $termslist = array_values( array_unique( array_intersect( array_map( 'sanitize_key', $termslist ), Kind_Taxonomy::get_kind_list() ) ) );
     122                return array(
     123                        'title'     => isset( $new_instance['title'] ) && is_string( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '',
     124                        'termslist' => $termslist,
     125                        'count'     => empty( $new_instance['count'] ) ? 0 : 1,
     126                        'all'       => empty( $new_instance['all'] ) ? 0 : 1,
     127                );
    103128        }
    104129
    … …  
    106131        /**
    107132         * Create the form for the Widget admin
     133         *
     134         * @since 3.1.3
    108135         *
    109136         * @see WP_Widget::form()
    … …  
    120147                $termslist = (array) $instance['termslist'];
    121148                ?>
    122                                 <p><label for="title"><?php esc_html_e( 'Title: ', 'indieweb-post-kinds' ); ?></label>
    123                                 <input type="text" size="30" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?> id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" value="
    124                 <?php echo esc_html( ( $instance['title'] ?? '' ) ); ?>" /></p>
     149                                <p><label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title: ', 'indieweb-post-kinds' ); ?></label>
     150                                <input type="text" size="30" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" value="<?php echo esc_attr( $instance['title'] ?? '' ); ?>" /></p>
    125151                <div id="kind-all">
    126152                <?php
    127                 foreach ( get_option( 'kind_termslist', Kind_Taxonomy::get_kind_list() ) as $term ) {
     153                foreach ( Kind_Config::get_termslist() as $term ) {
    128154                        $value = Kind_Taxonomy::get_post_kind_info( $term );
    129155                        if ( $value->show ) {
    … …  
    135161                                        checked( in_array( $term, $termslist, true ), true, false )
    136162                                );
    137                                 printf( '%1$s %2$s<br />', Kind_Taxonomy::get_icon( $term ), esc_html( $value->singular_name ) ); //phpcs:ignore
     163                                printf( '%1$s %2$s<br />', Kind_Taxonomy::get_icon( $term ), esc_html( $value->singular_name ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon markup.
    138164                        }
    139165                }
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-metabox.php

    r3001401 r3738687  
    11<?php
    22/**
    3  * Post Kind Post MetaBox Class
     3 * Kind_Metabox class.
    44 *
    5  * Sets Up Tabbed Metabox in the Posting UI for Kind data.
     5 * @package Post_Kinds
     6 * @since   2.2.0
    67 */
    78
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Adds the tabbed Post Kinds metabox to the Classic Editor and saves its fields.
     13 *
     14 * @since 2.2.0
     15 */
    816class Kind_Metabox {
    917
    1018        /**
     19         * Plugin version, used to version enqueued assets.
     20         *
    1121         * @var string $version
    1222         */
    … …  
    1525        /**
    1626         * Function to initiate our metabox.
     27         *
     28         * @since 2.2.0
    1729         *
    1830         * @access public
    … …  
    2133                self::$version = Post_Kinds_Plugin::$version;
    2234                add_action( 'edit_form_after_title', array( static::class, 'after_title_metabox' ) );
    23                 // Add meta box to new post/post pages only
     35                // Add meta box to new post/post pages only.
    2436                add_action( 'load-post.php', array( static::class, 'kindbox_setup' ) );
    2537                add_action( 'load-post-new.php', array( static::class, 'kindbox_setup' ) );
    … …  
    3345         * Function to change our post kind.
    3446         *
     47         * @since 3.1.0
     48         *
    3549         * @access public
    3650         *
    … …  
    6882
    6983        /**
    70          * Function to change if a new post should be considered empty.
     84         * Filters whether a post should be considered empty.
     85         *
     86         * Many kinds (likes, bookmarks, reposts, and so on) legitimately have no
     87         * content, title or excerpt, which core would otherwise reject as empty.
     88         *
     89         * @since 3.0.0
    7190         *
    7291         * @access public
    7392         *
    7493         * @param bool  $maybe_empty Whether or not the post should be considered empty.
    75          * @param array $postarr     Data for the post to be inserted.
     94         * @param array $postarr     Data for the post to be inserted or updated.
    7695         *
    7796         * @return bool
    7897         */
    7998        public static function wp_insert_post_empty_content( $maybe_empty, $postarr ) {
    80                 // Always let updates to trash posts through
    81                 if ( 'trash' === $postarr['post_status'] ) {
     99                // Always let updates to trash posts through.
     100                if ( isset( $postarr['post_status'] ) && 'trash' === $postarr['post_status'] ) {
    82101                        return false;
    83102                }
    84                 // Let All Micropub Posts through
    85                 if ( isset( $postarr['meta_input'] ) && isset( $postarr['meta_input']['micropub_auth_response'] ) ) {
     103                // Let all Micropub creates through.
     104                if ( isset( $postarr['meta_input']['micropub_auth_response'] ) ) {
    86105                        return false;
    87106                }
    88                 if ( ! isset( $postarr['tax_input'] ) && ! isset( $postarr['tax_input']['kind'] ) ) {
    89                         return $maybe_empty;
    90                 }
    91                 $kind = get_term_by( 'id', $postarr['tax_input']['kind'][0], 'kind' );
    92                 $kind = ( $kind instanceof WP_Term ) ? $kind->slug : '';
    93                 // Use traditional rules for articles
     107                $kind = self::kind_from_postarr( $postarr );
     108                // Use traditional rules for articles, and for posts without a kind.
    94109                if ( 'article' === $kind || ! $kind ) {
    95110                        return $maybe_empty;
    96111                }
    97                 $keys = array( 'cite_url', 'cite_name', 'cite_summary' );
    98                 $keys = array_flip( $keys );
    99                 $diff = array_filter( array_intersect_key( $_POST, $keys ) );
    100                 if ( ! empty( $diff ) ) {
     112                // An existing post of a kind other than article may have no content,
     113                // for example when Micropub updates a like.
     114                if ( ! empty( $postarr['ID'] ) ) {
    101115                        return false;
    102116                }
     117                // A new post from the metabox needs something to respond to.
     118                foreach ( array( 'cite_url', 'cite_name', 'cite_summary' ) as $key ) {
     119                        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Only checks whether a field was filled in; save_post() verifies the nonce before saving.
     120                        if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) && '' !== sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) ) {
     121                                return false;
     122                        }
     123                }
    103124                return $maybe_empty;
    104125        }
    105126
    106127        /**
     128         * Returns the kind slug for post data being inserted or updated.
     129         *
     130         * The kind comes from tax_input, which the kind metabox submits as a slug and
     131         * other callers may pass as term IDs, or failing that from the existing post.
     132         *
     133         * @since 4.0.0
     134         *
     135         * @access public
     136         *
     137         * @param array $postarr Data for the post to be inserted or updated.
     138         * @return string Kind slug, or an empty string if none.
     139         */
     140        public static function kind_from_postarr( $postarr ) {
     141                if ( isset( $postarr['tax_input']['kind'] ) ) {
     142                        $kind = $postarr['tax_input']['kind'];
     143                        if ( is_array( $kind ) ) {
     144                                $kind = reset( $kind );
     145                        }
     146                        $term = is_numeric( $kind ) ? get_term_by( 'id', (int) $kind, 'kind' ) : get_term_by( 'slug', (string) $kind, 'kind' );
     147                        return ( $term instanceof WP_Term ) ? $term->slug : '';
     148                }
     149                if ( ! empty( $postarr['ID'] ) ) {
     150                        $kind = get_post_kind_slug( $postarr['ID'] );
     151                        return $kind ? $kind : '';
     152                }
     153                return '';
     154        }
     155
     156        /**
    107157         * Execute metaboxes for the current screen, after the post title.
    108158         *
     159         * @since 3.0.0
     160         *
    109161         * @access public
    110162         * @param WP_Post $post Post object for the current screen.
    … …  
    117169        /**
    118170         * Metabox setup.
     171         *
     172         * @since 2.2.0
    119173         *
    120174         * @access public
    … …  
    124178                /* Add meta boxes on the 'add_meta_boxes' hook. */
    125179                add_action( 'add_meta_boxes', array( $cls, 'add_meta_boxes' ) );
     180                add_filter( 'hidden_meta_boxes', array( $cls, 'hidden_meta_boxes' ), 10, 2 );
    126181                add_action( 'admin_enqueue_scripts', array( $cls, 'enqueue_admin_scripts' ) );
    127182        }
    … …  
    129184        /**
    130185         * Enqueue our needed assets.
     186         *
     187         * @since 2.2.0
    131188         *
    132189         * @access public
    … …  
    145202
    146203                        // Provide a global object to our JS file containing our REST API endpoint, and API nonce
    147                         // Nonce must be 'wp_rest'
     204                        // Nonce must be 'wp_rest'.
    148205                        wp_localize_script(
    149206                                'kindmeta',
    … …  
    154211                                        'success_message' => __( 'Your URL has been successfully retrieved and parsed', 'indieweb-post-kinds' ),
    155212                                        'clear_message'   => __( 'Are you sure you want to clear post properties?', 'indieweb-post-kinds' ),
     213                                        'error_message'   => __( 'Error: Unable to Retrieve', 'indieweb-post-kinds' ),
     214                                        'invalid_url'     => __( 'Invalid URL', 'indieweb-post-kinds' ),
     215                                        'media_title'     => __( 'Attach', 'indieweb-post-kinds' ),
     216                                        'media_button'    => __( 'Use this media', 'indieweb-post-kinds' ),
    156217                                )
    157218                        );
    … …  
    163224        /**
    164225         * Utility function to concatenate a list of post kinds.
     226         *
     227         * @since 2.7.5
    165228         *
    166229         * @access public
    … …  
    180243         * Utility function to separate out a list of post kinds.
    181244         *
     245         * @since 2.7.5
     246         *
    182247         * @access public
    183248         *
    … …  
    194259        /**
    195260         * Function to render date/time field inputs.
     261         *
     262         * @since 2.5.0
    196263         *
    197264         * @access public
    … …  
    206273                $tz_seconds = get_option( 'gmt_offset' ) * 3600;
    207274                $offset     = tz_seconds_to_offset( $tz_seconds );
    208                 $time       = divide_datetime( $datetime );
     275                $time       = Kind_Time::divide_datetime( $datetime );
    209276                if ( ! is_array( $time ) ) {
    210277                        $time = array();
    … …  
    213280                        $offset = $time['offset'];
    214281                }
    215                 $string  = '<label class="half ' . $class . '" for="' . $prefix . '">' . $label . '<br/>';
    216                 $string .= '<input class="date" type="date" name="' . $prefix . '_date" id="' . $prefix . '_date" value="' . ( $time['date'] ?? '' ) . '"/>';
    217                 $string .= '<input class="time" type="time" name="' . $prefix . '_time" id="' . $prefix . '_time" step="1" value="' . ( $time['time'] ?? '' ) . '"/>';
     282                $prefix  = esc_attr( $prefix );
     283                $string  = '<label class="half ' . esc_attr( $class ) . '" for="' . $prefix . '">' . esc_html( $label ) . '<br/>';
     284                $string .= '<input class="date" type="date" name="' . $prefix . '_date" id="' . $prefix . '_date" value="' . esc_attr( $time['date'] ?? '' ) . '"/>';
     285                $string .= '<input class="time" type="time" name="' . $prefix . '_time" id="' . $prefix . '_time" step="1" value="' . esc_attr( $time['time'] ?? '' ) . '"/>';
    218286                $string .= self::select_offset( $prefix, $offset );
    219287                $string .= '</label>';
    … …  
    224292         * Function to render our timezone choices.
    225293         *
     294         * @since 2.5.0
     295         *
    226296         * @access public
    227297         *
    … …  
    231301         */
    232302        public static function select_offset( $prefix, $select ) {
    233                 $string  = '<select name="' . $prefix . '_offset" id="' . $prefix . '_offset">';
     303                $string  = '<select name="' . esc_attr( $prefix ) . '_offset" id="' . esc_attr( $prefix ) . '_offset">';
    234304                $string .= self::timezone_offset_choice( $select );
    235305                $string .= '</select>';
    … …  
    240310         * Function to render options for a chosen timezone select field.
    241311         *
     312         * @since 3.0.0
     313         *
    242314         * @access public
    243315         *
    … …  
    248320                $tzlist = get_gmt_offsets();
    249321                $string = '';
    250                 foreach ( $tzlist as $key => $value ) {
    251                         $string .= '<option value="' . $value . '"';
     322                foreach ( $tzlist as $value ) {
     323                        $string .= '<option value="' . esc_attr( $value ) . '"';
    252324                        if ( $select === $value ) {
    253325                                $string .= ' selected';
    254326                        }
    255                         $string .= '>GMT' . $value . '</option>';
     327                        $string .= '>GMT' . esc_html( $value ) . '</option>';
    256328                }
    257329                return $string;
    … …  
    261333         * Render the options for the rating select field.
    262334         *
    263          * @access public
    264          *
    265          * @param string $selected Selected Rating choice
     335         * @since 3.7.0
     336         *
     337         * @access public
     338         *
     339         * @param string $selected Selected Rating choice.
    266340         * @return string
    267341         */
    … …  
    278352                $ret      = '';
    279353                foreach ( $ratings as $key => $value ) {
    280                         $ret .= '<option value="' . $key . '"';
     354                        $ret .= '<option value="' . esc_attr( $key ) . '"';
    281355                        if ( $selected === $key ) {
    282356                                $ret .= ' selected';
    283357                        }
    284                         $ret .= '>' . $value . '</option>';
     358                        $ret .= '>' . esc_html( (string) $value ) . '</option>';
    285359                }
    286360                return $ret;
    … …  
    290364         * Render the options for the RSVP select field.
    291365         *
    292          * @access public
    293          *
    294          * @param string $selected Selected RSVP choice
     366         * @since 3.0.0
     367         *
     368         * @access public
     369         *
     370         * @param string $selected Selected RSVP choice.
    295371         * @return string
    296372         */
    … …  
    306382                $string = '';
    307383                foreach ( $rsvps as $key => $value ) {
    308                         $string .= '<option value="' . $key . '"';
     384                        $string .= '<option value="' . esc_attr( $key ) . '"';
    309385                        if ( $selected === $key ) {
    310386                                $string .= ' selected';
    311387                        }
    312                         $string .= '>' . $value . '</option>';
     388                        $string .= '>' . esc_html( (string) $value ) . '</option>';
    313389                }
    314390                return $string;
    … …  
    317393        /**
    318394         * Render our RSVP select input.
     395         *
     396         * @since 2.5.0
    319397         *
    320398         * @access public
    … …  
    324402         */
    325403        public static function rsvp_select( $selected ) {
    326                 $string  = '<label for="mf2_rsvp">' . __( 'RSVP', 'indieweb-post-kinds' ) . '</label><br/>';
     404                $string  = '<label for="mf2_rsvp">' . esc_html__( 'RSVP', 'indieweb-post-kinds' ) . '</label><br/>';
    327405                $string .= '<select name="mf2_rsvp" id="mf2_rsvp">';
    328406                $string .= self::rsvp_choice( $selected );
    … …  
    334412         * Render our rating select input.
    335413         *
     414         * @since 3.7.0
     415         *
    336416         * @access public
    337417         *
    … …  
    340420         */
    341421        public static function rating_select( $selected ) {
    342                 $string  = '<label for="mf2_rating">' . __( 'Rating', 'indieweb-post-kinds' ) . '</label><br/>';
     422                $string  = '<label for="mf2_rating">' . esc_html__( 'Rating', 'indieweb-post-kinds' ) . '</label><br/>';
    343423                $string .= '<select name="mf2_rating" id="mf2_rating">';
    344424                $string .= self::rating_choice( $selected );
    … …  
    348428
    349429        /**
     430         * Keeps the Response Properties box from being saved as hidden.
     431         *
     432         * The editor script hides the box for kinds without a response, such as
     433         * note. WordPress saves every hidden box as unticked in Screen Options
     434         * whenever any box is collapsed or any Screen Options box is toggled, so
     435         * the box would stay hidden for every kind afterwards. The script shows
     436         * and hides it by kind instead.
     437         *
     438         * @since 4.0.0
     439         *
     440         * @param string[]  $hidden IDs of the hidden meta boxes.
     441         * @param WP_Screen $screen The current screen.
     442         * @return string[] IDs of the hidden meta boxes.
     443         */
     444        public static function hidden_meta_boxes( $hidden, $screen ) {
     445                if ( ! is_array( $hidden ) || ! $screen instanceof WP_Screen || 'post' !== $screen->id ) {
     446                        return $hidden;
     447                }
     448                return array_values( array_diff( $hidden, array( 'replybox-meta' ) ) );
     449        }
     450
     451        /**
    350452         * Create one or more meta boxes to be displayed on the post editor screen.
     453         *
     454         * @since 2.2.0
    351455         *
    352456         * @access public
    … …  
    354458        public static function add_meta_boxes() {
    355459                add_meta_box(
    356                         'replybox-meta', // Unique ID
    357                         esc_html__( 'Response Properties', 'indieweb-post-kinds' ), // Title
    358                         array( static::class, 'reply_metabox' ), // Callback function
     460                        'replybox-meta', // Unique ID.
     461                        esc_html__( 'Response Properties', 'indieweb-post-kinds' ), // Title.
     462                        array( static::class, 'reply_metabox' ), // Callback function.
    359463                        'post',
    360                         'kind_after_title', // Context
    361                         'default', // Priority
     464                        'kind_after_title', // Context.
     465                        'default', // Priority.
    362466                        array(
    363467                                '__block_editor_compatible_meta_box' => false,
    … …  
    370474         * Render our reply meta box.
    371475         *
     476         * @since 3.0.0
     477         *
    372478         * @access public
    373479         *
    … …  
    382488         * Process and save meta box data.
    383489         *
    384          * @access public
    385          *
    386          * @param int    $post_id Saved post ID.
    387          * @param WP_Pos $post    Saved post object.
     490         * @since 2.2.0
     491         *
     492         * @access public
     493         *
     494         * @param int     $post_id Saved post ID.
     495         * @param WP_Post $post    Saved post object.
    388496         */
    389497        public static function save_post( $post_id, $post ) {
    … …  
    399507
    400508                // Verify that the nonce is valid.
    401                 if ( ! wp_verify_nonce( $_POST['replykind_metabox_nonce'], 'replykind_metabox' ) ) {
     509                if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['replykind_metabox_nonce'] ) ), 'replykind_metabox' ) ) {
    402510                        return;
    403511                }
    … …  
    409517
    410518                // Check the user's permissions.
    411                 if ( isset( $_POST['post_type'] ) && 'page' === $_POST['post_type'] ) {
     519                if ( 'page' === self::posted_text( 'post_type' ) ) {
    412520                        if ( ! current_user_can( 'edit_page', $post_id ) ) {
    413521                                return;
    … …  
    422530
    423531                if ( isset( $_POST['mf2_start_date'] ) || isset( $_POST['mf2_start_time'] ) ) {
    424                         $start = build_iso8601_time( sanitize_text_field( $_POST['mf2_start_date'] ), sanitize_text_field( $_POST['mf2_start_time'] ), sanitize_text_field( $_POST['mf2_start_offset'] ) );
     532                        $start = build_iso8601_time( self::posted_text( 'mf2_start_date' ), self::posted_text( 'mf2_start_time' ), self::posted_text( 'mf2_start_offset' ) );
    425533                        if ( ! $start ) {
    426534                                $kind_post->delete( 'start' );
    … …  
    430538                }
    431539                if ( isset( $_POST['mf2_end_date'] ) || isset( $_POST['mf2_end_time'] ) ) {
    432                         $end = build_iso8601_time( sanitize_text_field( $_POST['mf2_end_date'] ), sanitize_text_field( $_POST['mf2_end_time'] ), sanitize_text_field( $_POST['mf2_end_offset'] ) );
     540                        $end = build_iso8601_time( self::posted_text( 'mf2_end_date' ), self::posted_text( 'mf2_end_time' ), self::posted_text( 'mf2_end_offset' ) );
    433541                        if ( ! $end ) {
    434542                                $kind_post->delete( 'end' );
    … …  
    443551
    444552                $durations = array(
    445                         'Y' => intval( $_POST['duration_years'] ?? '' ),
    446                         'M' => intval( $_POST['duration_months'] ?? '' ),
    447                         'D' => intval( $_POST['duration_days'] ?? '' ),
    448                         'H' => intval( $_POST['duration_hours'] ?? '' ),
    449                         'I' => intval( $_POST['duration_minutes'] ?? '' ),
    450                         'S' => intval( $_POST['duration_seconds'] ?? '' ),
     553                        'Y' => absint( self::posted_text( 'duration_years' ) ),
     554                        'M' => absint( self::posted_text( 'duration_months' ) ),
     555                        'D' => absint( self::posted_text( 'duration_days' ) ),
     556                        'H' => absint( self::posted_text( 'duration_hours' ) ),
     557                        'I' => absint( self::posted_text( 'duration_minutes' ) ),
     558                        'S' => absint( self::posted_text( 'duration_seconds' ) ),
    451559                );
    452560                $duration  = build_iso8601_duration( $durations );
    … …  
    456564                        if ( $duration instanceof DateInterval ) {
    457565                                $duration = date_interval_to_iso8601( $duration );
    458                                 error_log( wp_json_encode( $duration ) );
    459566                        }
    460567                }
    … …  
    465572                }
    466573
    467                 $kind_post->set( 'rsvp', $_POST['mf2_rsvp'] );
    468                 if ( array_key_exists( 'mf2_rating', $_POST ) && is_numeric( $_POST['mf2_rating'] ) ) {
    469                         $kind_post->set( 'rating', intval( $_POST['mf2_rating'] ) );
    470                 }
    471 
    472                 if ( isset( $_POST['cite_published_date'] ) || isset( $_POST['published_time'] ) ) {
    473                         $cite['published'] = build_iso8601_time( sanitize_text_field( $_POST['cite_published_date'] ), sanitize_text_field( $_POST['cite_published_time'] ), sanitize_text_field( $_POST['cite_published_offset'] ) );
     574                if ( isset( $_POST['mf2_rsvp'] ) ) {
     575                        $rsvp = self::posted_text( 'mf2_rsvp' );
     576                        if ( in_array( $rsvp, array( 'yes', 'no', 'maybe', 'interested', 'remote' ), true ) ) {
     577                                $kind_post->set( 'rsvp', $rsvp );
     578                        } else {
     579                                $kind_post->delete( 'rsvp' );
     580                        }
     581                }
     582                $rating = self::posted_text( 'mf2_rating' );
     583                if ( is_numeric( $rating ) ) {
     584                        $kind_post->set( 'rating', intval( $rating ) );
     585                }
     586
     587                if ( isset( $_POST['cite_published_date'] ) || isset( $_POST['cite_published_time'] ) ) {
     588                        $cite['published'] = build_iso8601_time( self::posted_text( 'cite_published_date' ), self::posted_text( 'cite_published_time' ), self::posted_text( 'cite_published_offset' ) );
    474589                }
    475590                if ( isset( $_POST['cite_updated_date'] ) || isset( $_POST['cite_updated_time'] ) ) {
    476                         $cite['updated'] = build_iso8601_time( sanitize_text_field( $_POST['cite_updated_date'] ), sanitize_text_field( $_POST['cite_updated_time'] ), sanitize_text_field( $_POST['cite_updated_offset'] ) );
    477                 }
    478                 $cite['summary'] = wp_kses_post( $_POST['cite_summary'] ?? '' );
    479                 $cite['name']    = sanitize_text_field( $_POST['cite_name'] ?? '' );
    480                 $cite['url']     = esc_url( $_POST['cite_url'] ?? '' );
     591                        $cite['updated'] = build_iso8601_time( self::posted_text( 'cite_updated_date' ), self::posted_text( 'cite_updated_time' ), self::posted_text( 'cite_updated_offset' ) );
     592                }
     593                // Summaries are plain text; line breaks are kept.
     594                $cite['summary'] = isset( $_POST['cite_summary'] ) && is_string( $_POST['cite_summary'] ) ? sanitize_textarea_field( wp_unslash( $_POST['cite_summary'] ) ) : '';
     595                $cite['name']    = self::posted_text( 'cite_name' );
     596                $cite['url']     = self::posted_urls( 'cite_url' );
    481597                if ( isset( $_POST['cite_tags'] ) ) {
    482                         $cite['category'] = array_filter( explode( ';', sanitize_text_field( $_POST['cite_tags'] ) ) );
    483                 }
    484                 $cite['publication'] = sanitize_text_field( $_POST['cite_publication'] ?? '' );
    485                 $cite['featured']    = esc_url( $_POST['cite_featured'] ?? '' );
    486 
    487                 $author         = array();
    488                 $author['name'] = self::explode( sanitize_text_field( $_POST['cite_author_name'] ?? '' ) );
    489                 $author['url']  = self::explode( $_POST['cite_author_url'] ?? '' );
    490                 if ( is_array( $author['url'] ) ) {
    491                         $author['url'] = array_map( 'esc_url', $author['url'] );
     598                        $cite['category'] = array_values( array_filter( array_map( 'trim', explode( ';', self::posted_text( 'cite_tags' ) ) ) ) );
     599                }
     600                $cite['publication'] = self::posted_text( 'cite_publication' );
     601                $cite['featured']    = self::posted_urls( 'cite_featured' );
     602
     603                // One h-card per author. Like the other fields, an empty value clears what was stored.
     604                $authors = array_map( '\ParseThis\jf2_to_mf2', self::posted_authors() );
     605                if ( empty( $authors ) ) {
     606                        $cite['author'] = '';
    492607                } else {
    493                         $author['url'] = esc_url( $author['url'] );
    494                 }
    495 
    496                 $author['photo'] = self::explode( $_POST['cite_author_photo'] ?? '' );
    497 
    498                 if ( is_array( $author['photo'] ) ) {
    499                         $author['photo'] = array_map( 'esc_url', $author['photo'] );
    500                 } else {
    501                         $author['photo'] = esc_url( $author['photo'] );
    502                 }
    503 
    504                 $author = array_filter( $author );
    505                 if ( ! empty( $author ) ) {
    506 
    507                         $author['type'] = 'card';
    508                         $cite['author'] = jf2_to_mf2( $author );
    509                 }
     608                        $cite['author'] = 1 === count( $authors ) ? $authors[0] : $authors;
     609                }
     610
    510611                $kind = $kind_post->get_kind();
    511612                $type = Kind_Taxonomy::get_kind_info( $kind, 'property' );
    512                 // Make sure there is no overwrite of properties that might not be handled by the plugin
    513                 $fetch = $kind_post->get_cite();
    514                 if ( ! $fetch ) {
    515                         $fetch = array();
    516                 } else {
    517                         $fetch = array_filter( $fetch );
    518                 }
     613                // Make sure there is no overwrite of properties that might not be handled by the plugin.
     614                $fetch = self::stored_cite_properties( $kind_post->get_cite() );
    519615                if ( empty( $_POST['cite_media'] ) ) {
    520616                        $cite = array_merge( $fetch, $cite );
    … …  
    531627                                }
    532628                                $cite = array( 'properties' => $build );
    533                                 // Temporary code which assumes everything except a checkin is a citation
     629                                // Temporary code which assumes everything except a checkin is a citation.
    534630                                if ( 'checkin' === $kind ) {
    535631                                        $cite['type'] = array( 'h-card' );
    … …  
    541637                        }
    542638                }
    543                 $kind_post->set( $type, $cite );
     639                // Values are unslashed for sanitizing; the metadata API expects them slashed.
     640                $kind_post->set( $type, wp_slash( $cite ) );
     641        }
     642
     643        /**
     644         * Returns a posted metabox field as sanitized text.
     645         *
     646         * Only called from save_post(), after the nonce is verified.
     647         *
     648         * @since 4.0.0
     649         *
     650         * @param string $key Field name.
     651         * @return string The unslashed, sanitized value, or an empty string if it is missing.
     652         */
     653        private static function posted_text( $key ) {
     654                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in save_post().
     655                if ( ! isset( $_POST[ $key ] ) || ! is_string( $_POST[ $key ] ) ) {
     656                        return '';
     657                }
     658                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in save_post().
     659                return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
     660        }
     661
     662        /**
     663         * Returns the properties of a stored citation, keyed by property name.
     664         *
     665         * Saving merges these with the posted fields, so properties the metabox
     666         * does not edit are kept. A citation stored as a plain URL has none.
     667         * Before 4.0.0, saving merged the whole stored microformat instead, nesting
     668         * it inside its own properties one level deeper on each save; those levels
     669         * are flattened here, newer values winning.
     670         *
     671         * @since 4.0.0
     672         *
     673         * @param mixed $cite Citation as returned by Kind_Post::get_cite().
     674         * @return array Property values, each a list.
     675         */
     676        private static function stored_cite_properties( $cite ) {
     677                if ( wp_is_numeric_array( $cite ) && 1 === count( $cite ) ) {
     678                        $cite = $cite[0];
     679                }
     680                if ( ! is_array( $cite ) || empty( $cite['properties'] ) || ! is_array( $cite['properties'] ) ) {
     681                        return array();
     682                }
     683                return array_filter( Kind_Upgrade::flatten_properties( $cite['properties'] ) );
     684        }
     685
     686        /**
     687         * Returns a posted metabox URL field, sanitized.
     688         *
     689         * Only called from save_post(), after the nonce is verified.
     690         *
     691         * @since 4.0.0
     692         *
     693         * @param string $key Field name.
     694         * @return string The URL, or an empty string if it is not valid.
     695         */
     696        private static function posted_urls( $key ) {
     697                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in save_post().
     698                if ( ! isset( $_POST[ $key ] ) || ! is_string( $_POST[ $key ] ) ) {
     699                        return '';
     700                }
     701                // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified in save_post(); sanitized with sanitize_url() (sanitize_text_field() would alter %-encoding).
     702                return sanitize_url( trim( wp_unslash( $_POST[ $key ] ) ) );
     703        }
     704
     705        /**
     706         * Returns the authors posted in the metabox's Author tab, as jf2 h-cards.
     707         *
     708         * Each field holds one value per author, separated by semicolons, so the
     709         * first name goes with the first URL and the first photo, and so on. An
     710         * author without a URL leaves that position empty, as in "Ann; Bob" and
     711         * "; https://example.com/bob".
     712         *
     713         * Only called from save_post(), after the nonce is verified.
     714         *
     715         * @since 4.0.0
     716         *
     717         * @return array[] The authors, each with type 'card' and any of name, url and photo.
     718         */
     719        private static function posted_authors() {
     720                $fields = array();
     721                foreach ( array(
     722                        'name'  => 'cite_author_name',
     723                        'url'   => 'cite_author_url',
     724                        'photo' => 'cite_author_photo',
     725                ) as $property => $key ) {
     726                        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified in save_post(); each value is sanitized below.
     727                        $value  = isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : '';
     728                        $values = array_map( 'trim', explode( ';', $value ) );
     729
     730                        $fields[ $property ] = array_map( 'name' === $property ? 'sanitize_text_field' : 'sanitize_url', $values );
     731                }
     732
     733                $authors = array();
     734                $count   = max( array_map( 'count', $fields ) );
     735                for ( $i = 0; $i < $count; $i++ ) {
     736                        $author = array();
     737                        foreach ( $fields as $property => $values ) {
     738                                if ( isset( $values[ $i ] ) && '' !== $values[ $i ] ) {
     739                                        $author[ $property ] = $values[ $i ];
     740                                }
     741                        }
     742                        if ( $author ) {
     743                                $authors[] = array_merge( array( 'type' => 'card' ), $author );
     744                        }
     745                }
     746                return $authors;
    544747        }
    545748
    546749        /**
    547750         * Function to handle saving our kind data upon post status transition.
     751         *
     752         * @since 2.2.0
    548753         *
    549754         * @access public
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-plugins.php

    r2973430 r3738687  
    11<?php
    2 
    32/**
    4  * Post Kind Plugins Class
     3 * Kind_Plugins class.
    54 *
    6  * Custom Functions for Specific Other Pugins
     5 * @package Post_Kinds
     6 * @since   2.5.2
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Integrations with other plugins, such as Micropub and ActivityPub.
    713 *
    8  * @package Post Kinds
     14 * @since 2.5.2
    915 */
    1016class Kind_Plugins {
    1117
    1218        /**
     19         * WP-Cron hook that retries parsing a Micropub citation in the background.
     20         *
     21         * @var string
     22         */
     23        const ENRICH_EVENT = 'post_kinds_enrich_citation';
     24
     25        /**
     26         * How many times a citation is parsed after temporary failures, counting the
     27         * attempt during the request.
     28         *
     29         * @var int
     30         */
     31        const ENRICH_ATTEMPTS = 3;
     32
     33        /**
     34         * Post meta holding the log of citation parsing outcomes for a post.
     35         *
     36         * @var string
     37         */
     38        const LOG_META = '_kind_citation_log';
     39
     40        /**
     41         * Post meta holding the post's citation status: retrying or failed. It is
     42         * removed when no citation is retrying or failed.
     43         *
     44         * @var string
     45         */
     46        const STATUS_META = '_kind_citation_status';
     47
     48        /**
     49         * How many log entries are kept per post.
     50         *
     51         * @var int
     52         */
     53        const LOG_LIMIT = 20;
     54
     55        /**
     56         * Citation URLs from the current Micropub request that could not be parsed,
     57         * as arrays of property, URL and WP_Error, waiting for the post ID.
     58         *
     59         * @var array[]
     60         */
     61        private static $pending = array();
     62
     63        /**
    1364         * Initialize our plugin integrations.
    1465         *
     66         * @since 2.5.2
     67         *
    1568         * @access public
    1669         */
    1770        public static function init() {
    18                 // Set Post Kind for Micropub Inputs.
     71                // Set the post kind for Micropub posts before they are inserted (Micropub 2.1.0
     72                // and later), with after_micropub as a fallback.
     73                add_filter( 'micropub_tax_input', array( static::class, 'micropub_tax_input' ), 10, 2 );
    1974                add_action( 'after_micropub', array( static::class, 'micropub_set_kind' ), 9, 2 );
    2075                add_action( 'after_micropub', array( static::class, 'post_formats' ), 11, 2 );
    2176                add_filter( 'before_micropub', array( static::class, 'micropub_parse' ), 11 );
     77                add_action( 'after_micropub', array( static::class, 'schedule_enrichment' ), 20, 2 );
     78                add_action( self::ENRICH_EVENT, array( static::class, 'enrich_citation' ), 10, 4 );
     79                // Advertise the enabled kinds to Micropub clients (Micropub 1.4 and later).
     80                add_filter( 'micropub_query', array( static::class, 'micropub_query' ), 10, 2 );
    2281                add_filter( 'tempus_widget_post_title', array( static::class, 'tempus_widget_post_title' ), 10, 2 );
    2382                // Override Post Type in Semantic Linkbacks.
    2483                add_filter( 'semantic_linkbacks_post_type', array( static::class, 'semantic_post_type' ), 11, 2 );
    2584
    26                 // Remove the Automatic Post Generation that the Micropub Plugin Offers
    27                 if ( class_exists( 'Micropub_Render' ) ) {
    28                         if ( has_filter( 'micropub_post_content', array( 'Micropub_Render', 'generate_post_content' ) ) ) {
    29                                 remove_filter( 'micropub_post_content', array( 'Micropub_Render', 'generate_post_content' ), 1, 2 );
    30                         }
    31                 } elseif ( class_exists( 'Micropub_Plugin' ) ) {
    32                         if ( has_filter( 'micropub_post_content', array( 'Micropub_Plugin', 'generate_post_content' ) ) ) {
    33                                 remove_filter( 'micropub_post_content', array( 'Micropub_Plugin', 'generate_post_content' ), 1, 2 );
    34                         }
    35                 }
    36 
    37                 // Hum Compatibility Filters
     85                // Post Kinds displays the response itself, so turn off Micropub's dynamic
     86                // rendering of Micropub posts (Micropub 2.4.0 and later).
     87                add_filter( 'micropub_dynamic_render', '__return_false' );
     88
     89                // Hum Compatibility Filters.
    3890                add_action( 'hum_local_types', array( static::class, 'hum_local_types' ), 11 );
    3991                add_action( 'hum_type_prefix', array( static::class, 'hum_type_prefix' ), 11, 2 );
    4092
    41                 // ActivityPub Filters
     93                // ActivityPub Filters.
    4294                add_filter( 'activitypub_post', array( static::class, 'activitypub_post' ), 12 );
    4395        }
    … …  
    4799         * Construct a title for the Tempus Widget link.
    48100         *
     101         * @since 3.5.4
     102         *
    49103         * @access public
    50104         *
    51          * @param string $title The Original Title.
    52          * @param WP_Post $post Post object.
     105         * @param string  $title The Original Title.
     106         * @param WP_Post $post  Post object.
    53107         * @return string
    54108         */
    … …  
    58112
    59113        /**
    60          *
    61          * @param array $post_array
     114         * Sets the ActivityPub object type based on the post kind.
     115         *
     116         * @since 3.6.2
     117         *
     118         * @param array $post_array ActivityPub object array.
    62119         *
    63120         * @return array
    … …  
    93150        }
    94151
     152        /**
     153         * Adds the kinds' shortlink type codes to the Hum plugin.
     154         *
     155         * @since 3.4.0
     156         *
     157         * @param string[] $types Local type codes.
     158         * @return string[] Local type codes.
     159         */
    95160        public static function hum_local_types( $types ) {
    96161                // http://tantek.pbworks.com/w/page/21743973/Whistle#design - Some of the uses are modified based on design considerations noted.
    97                 $types[] = 'f'; // Favorited, Likes, etc
    98                 $types[] = 'e'; // Events
    99                 $types[] = 'g'; // Geo Checkin
    100                 $types[] = 'h'; // Link
    101                 $types[] = 'm'; // Metric
    102                 $types[] = 'q'; // Question
    103                 $types[] = 'r'; // Review
    104                 $types[] = 'x'; // Experience
    105                 $types[] = 'u'; // Status Update
     162                $types[] = 'f'; // Favorited, Likes, etc.
     163                $types[] = 'e'; // Events.
     164                $types[] = 'g'; // Geo Checkin.
     165                $types[] = 'h'; // Link.
     166                $types[] = 'm'; // Metric.
     167                $types[] = 'q'; // Question.
     168                $types[] = 'r'; // Review.
     169                $types[] = 'x'; // Experience.
     170                $types[] = 'u'; // Status Update.
    106171                return $types;
    107172        }
    108173
     174        /**
     175         * Uses the kind's shortlink code as a post's Hum type prefix.
     176         *
     177         * @since 3.4.0
     178         *
     179         * @param string $prefix  Type prefix.
     180         * @param int    $post_id Post ID.
     181         * @return string The kind's shortlink code, or the prefix if it has none.
     182         */
    109183        public static function hum_type_prefix( $prefix, $post_id ) {
    110184                $post_type = get_post_type( $post_id );
    … …  
    124198         * Replaces need for replacing the entire excerpt.
    125199         *
     200         * @since 2.5.2
     201         *
    126202         * @access public
    127203         *
    … …  
    135211
    136212        /**
    137          * Take mf2 properties and set a post kind.
    138          * Implements Post Type Discovery https://www.w3.org/TR/post-type-discovery/
    139          *
    140          * @param array $input   Micropub Request in JSON.
    141          * @param array $wp_args Arguments passed to insert or update posts.
     213         * Returns the post kind for an mf2 object, using Post Type Discovery.
     214         * Implements https://www.w3.org/TR/post-type-discovery/ via Parse This.
     215         *
     216         * @since 4.0.0
     217         *
     218         * @param array $mf2 mf2 object, such as a Micropub request.
     219         * @return string Registered kind slug, or an empty string if none applies.
     220         */
     221        public static function discover_kind( $mf2 ) {
     222                if ( ! is_array( $mf2 ) || empty( $mf2['properties'] ) || ! function_exists( '\\ParseThis\\post_type_discovery' ) ) {
     223                        return '';
     224                }
     225                $type = \ParseThis\post_type_discovery( \ParseThis\mf2_to_jf2( $mf2 ) );
     226                return ( $type && Kind_Taxonomy::get_post_kind_info( $type ) ) ? $type : '';
     227        }
     228
     229        /**
     230         * Sets the kind of a Micropub post in the taxonomy input, so the post is
     231         * inserted with its kind rather than the default term. Everything that runs
     232         * during the insert, such as the publish transition, post formats and other
     233         * plugins' publish hooks, then sees the right kind.
     234         *
     235         * Hooked to Micropub's micropub_tax_input filter (Micropub 2.1.0 and later),
     236         * which is applied when a post is created.
     237         *
     238         * @since 4.0.0
     239         *
     240         * @param array|null $tax_input Taxonomy input for the new post.
     241         * @param array      $input     Micropub request.
     242         * @return array|null Taxonomy input, with the kind added.
     243         */
     244        public static function micropub_tax_input( $tax_input, $input ) {
     245                $kind = self::discover_kind( $input );
     246                if ( ! $kind ) {
     247                        return $tax_input;
     248                }
     249                if ( ! is_array( $tax_input ) ) {
     250                        $tax_input = array();
     251                }
     252                if ( empty( $tax_input['kind'] ) ) {
     253                        $tax_input['kind'] = array( $kind );
     254                }
     255                return $tax_input;
     256        }
     257
     258        /**
     259         * Sets the kind of a Micropub post after it is created, if it does not
     260         * already have it.
     261         *
     262         * The kind is normally set before the insert (see micropub_tax_input()). This
     263         * covers Micropub versions before 2.1.0, and users who cannot assign kind
     264         * terms, for whom WordPress ignores the taxonomy input.
     265         *
     266         * @since 2.5.2
     267         *
     268         * @param array      $input   Micropub request.
     269         * @param array|null $wp_args Arguments of the created or updated post; null for queries.
    142270         */
    143271        public static function micropub_set_kind( $input, $wp_args ) {
    144                 // Only continue if create or update
    145                 if ( ! $wp_args ) {
     272                if ( empty( $wp_args['ID'] ) ) {
    146273                        return;
    147274                }
    148                 $type = post_type_discovery( mf2_to_jf2( $input ) );
    149                 if ( ! empty( $type ) ) {
    150                         set_post_kind( $wp_args['ID'], $type );
    151                 }
    152         }
    153 
    154         /**
    155          * Set our post formats.
     275                $post_id = (int) $wp_args['ID'];
     276                if ( isset( $input['action'] ) && 'update' === $input['action'] ) {
     277                        // Only recalculate when the update changed a property that decides the
     278                        // kind, so a kind chosen in the editor survives unrelated updates.
     279                        if ( ! self::update_changes_kind( $input ) ) {
     280                                return;
     281                        }
     282                        $kind = self::discover_kind( self::stored_mf2( $post_id ) );
     283                } else {
     284                        $kind = self::discover_kind( $input );
     285                }
     286                if ( $kind && get_post_kind_slug( $post_id ) !== $kind ) {
     287                        self::set_kind_without_moving_properties( $post_id, $kind );
     288                }
     289        }
     290
     291        /**
     292         * Returns the properties that decide a post's kind: each registered kind's
     293         * property, plus rsvp, which Post Type Discovery checks first.
     294         *
     295         * @since 4.0.0
     296         *
     297         * @return string[] Property names.
     298         */
     299        public static function kind_properties() {
     300                $properties = array( 'rsvp' );
     301                foreach ( Kind_Taxonomy::get_kind_list() as $kind ) {
     302                        $property = Kind_Taxonomy::get_kind_info( $kind, 'property' );
     303                        if ( $property ) {
     304                                $properties[] = $property;
     305                        }
     306                }
     307                return array_values( array_unique( $properties ) );
     308        }
     309
     310        /**
     311         * Whether a Micropub update request replaces, adds or deletes a property
     312         * that decides the kind.
     313         *
     314         * @since 4.0.0
     315         *
     316         * @param array $input Micropub update request.
     317         * @return bool
     318         */
     319        public static function update_changes_kind( $input ) {
     320                $changed = array();
     321                foreach ( array( 'replace', 'add' ) as $operation ) {
     322                        if ( isset( $input[ $operation ] ) && is_array( $input[ $operation ] ) ) {
     323                                $changed = array_merge( $changed, array_keys( $input[ $operation ] ) );
     324                        }
     325                }
     326                if ( isset( $input['delete'] ) && is_array( $input['delete'] ) ) {
     327                        // Either a list of property names, or an object of property => values.
     328                        $changed = array_merge( $changed, wp_is_numeric_array( $input['delete'] ) ? $input['delete'] : array_keys( $input['delete'] ) );
     329                }
     330                return (bool) array_intersect( $changed, self::kind_properties() );
     331        }
     332
     333        /**
     334         * Builds the mf2 for a post from what is stored: the mf2_ post meta, plus the
     335         * title and content, which Post Type Discovery uses to tell articles from notes.
     336         *
     337         * @since 4.0.0
     338         *
     339         * @param int $post_id Post ID.
     340         * @return array mf2 object.
     341         */
     342        public static function stored_mf2( $post_id ) {
     343                $properties = array();
     344                foreach ( get_post_meta( $post_id ) as $key => $values ) {
     345                        if ( 0 !== strpos( $key, 'mf2_' ) || 'mf2_type' === $key ) {
     346                                continue;
     347                        }
     348                        $value = maybe_unserialize( $values[0] );
     349                        if ( '' === $value || array() === $value ) {
     350                                continue;
     351                        }
     352                        $properties[ substr( $key, 4 ) ] = is_array( $value ) ? $value : array( $value );
     353                }
     354                $post = get_post( $post_id );
     355                if ( $post && '' !== $post->post_title ) {
     356                        $properties['name'] = array( $post->post_title );
     357                }
     358                if ( $post && '' !== $post->post_content ) {
     359                        $properties['content'] = array( $post->post_content );
     360                }
     361                return array(
     362                        'type'       => array( 'h-entry' ),
     363                        'properties' => $properties,
     364                );
     365        }
     366
     367        /**
     368         * Sets a post's kind without moving property values between properties.
     369         *
     370         * When the kind changes in the editor, Kind_Metabox::change_kind() moves the
     371         * old kind's property to the new one (a like-of becomes an in-reply-to, for
     372         * example). After a Micropub update the properties are already where the
     373         * client put them, so that is suspended here.
     374         *
     375         * @since 4.0.0
     376         *
     377         * @param int    $post_id Post ID.
     378         * @param string $kind    Kind slug.
     379         */
     380        private static function set_kind_without_moving_properties( $post_id, $kind ) {
     381                $moving = has_action( 'change_kind', array( 'Kind_Metabox', 'change_kind' ) );
     382                if ( false !== $moving ) {
     383                        remove_action( 'change_kind', array( 'Kind_Metabox', 'change_kind' ), $moving );
     384                }
     385                set_post_kind( $post_id, $kind );
     386                if ( false !== $moving ) {
     387                        add_action( 'change_kind', array( 'Kind_Metabox', 'change_kind' ), $moving, 3 );
     388                }
     389        }
     390
     391        /**
     392         * Sets the post format of a post created or updated by Micropub to match its kind.
     393         *
     394         * @since 2.5.2
    156395         *
    157396         * @access public
    158397         *
    159          * @param $input
    160          * @param $wp_args
     398         * @param array $input   The Micropub request.
     399         * @param array $wp_args Arguments passed to wp_insert_post() or wp_update_post().
    161400         */
    162401        public static function post_formats( $input, $wp_args ) {
    … …  
    169408
    170409        /**
    171          * Parse our micropub values.
     410         * Returns the Micropub properties whose URL values are enriched into citations.
     411         *
     412         * @since 4.0.0
     413         *
     414         * @return string[] Property names.
     415         */
     416        public static function citation_properties() {
     417                return array( 'bookmark-of', 'like-of', 'favorite-of', 'in-reply-to', 'read-of', 'listen-of', 'watch-of' );
     418        }
     419
     420        /**
     421         * Enriches the URLs in citation properties of a Micropub create request, or
     422         * of the replace part of an update request, into h-cite objects parsed from
     423         * the cited page.
     424         *
     425         * Follows the Micropub spec: property values are arrays, so a property whose
     426         * value is not an array is left for Micropub to handle, and only string URLs
     427         * inside the array are enriched. Values the client already sent as objects,
     428         * such as an h-cite, are kept as sent.
     429         *
     430         * @since 2.7.5
    172431         *
    173432         * @access public
    174433         *
    175          * @param array $input Array of inputs to parse.
    176          * @return mixed
     434         * @param array $input Micropub request, in mf2 JSON.
     435         * @return array The request, with citation URLs enriched where possible.
    177436         */
    178437        public static function micropub_parse( $input ) {
    179                 if ( ! $input ) {
     438                self::$pending = array();
     439                // Queries (q) are left as is.
     440                if ( ! is_array( $input ) || isset( $input['q'] ) ) {
    180441                        return $input;
    181442                }
    182                 // q indicates a get query
    183                 if ( isset( $input['q'] ) ) {
     443                if ( ! class_exists( '\\ParseThis\\Parser' ) || ! self::enrichment_enabled( $input ) ) {
    184444                        return $input;
    185445                }
    186                 if ( ! isset( $input['properties'] ) ) {
    187                         return $input;
    188                 }
    189                 $parsed = array( 'bookmark-of', 'like-of', 'favorite-of', 'in-reply-to', 'read-of', 'listen-of', 'watch-of' );
    190                 foreach ( $input['properties'] as $property => $value ) {
    191                         if ( in_array( $property, $parsed, true ) ) {
    192                                 if ( wp_is_numeric_array( $value ) ) {
    193                                         foreach ( $value as $i => $v ) {
    194                                                 if ( wp_http_validate_url( $v ) ) {
    195                                                         $parse = new Parse_This( $v );
    196                                                         $fetch = $parse->fetch();
    197                                                         if ( ! is_wp_error( $fetch ) ) {
    198                                                                 $parse->parse();
    199                                                                 $jf2 = $parse->get();
    200                                                                 // Entries become citations
    201                                                                 if ( 'entry' === $jf2['type'] ) {
    202                                                                         $jf2['type'] = 'cite';
    203                                                                 }
    204                                                                 $mf2                                    = jf2_to_mf2( $jf2 );
    205                                                                 $input['properties'][ $property ][ $i ] = $mf2;
    206                                                         } else {
    207                                                                 error_log( wp_json_encode( $fetch ) ); // phpcs:ignore
    208                                                         }
    209                                                 }
    210                                         }
    211                                 } elseif ( isset( $value['url'] ) && wp_http_validate_url( $value['url'] ) ) {
    212                                         $parse = new Parse_This( $value['url'] );
    213                                         $fetch = $parse - fetch();
    214                                         if ( ! is_wp_error( $fetch ) ) {
    215                                                 $parse->parse();
    216                                                 $input['properties'][ $property ] = array_merge( $value, jf2_to_mf2( $parse->get() ) );
     446                // Create requests carry properties. Update requests can replace citation
     447                // properties; Micropub only allows adding categories, syndication and media.
     448                if ( isset( $input['properties'] ) && is_array( $input['properties'] ) ) {
     449                        $input['properties'] = self::enrich_properties( $input['properties'] );
     450                }
     451                if ( isset( $input['action'] ) && 'update' === $input['action'] && isset( $input['replace'] ) && is_array( $input['replace'] ) ) {
     452                        $input['replace'] = self::enrich_properties( $input['replace'] );
     453                }
     454                return $input;
     455        }
     456
     457        /**
     458         * Enriches the citation URLs in a set of Micropub properties.
     459         *
     460         * @since 4.0.0
     461         *
     462         * @param array $properties Properties, keyed by name, each an array of values.
     463         * @return array The properties, with citation URLs enriched where possible.
     464         */
     465        private static function enrich_properties( $properties ) {
     466                foreach ( self::citation_properties() as $property ) {
     467                        if ( empty( $properties[ $property ] ) || ! wp_is_numeric_array( $properties[ $property ] ) ) {
     468                                continue;
     469                        }
     470                        foreach ( $properties[ $property ] as $i => $value ) {
     471                                if ( ! is_string( $value ) || ! wp_http_validate_url( $value ) ) {
     472                                        continue;
     473                                }
     474                                // Keep the client waiting only briefly: a short timeout and no follow-up
     475                                // requests. Failures are logged, and retried in the background where
     476                                // worthwhile, once the post exists (see schedule_enrichment()).
     477                                $cite = self::parse_citation( $value, self::request_parse_args() );
     478                                if ( is_wp_error( $cite ) ) {
     479                                        self::report_failure( $value, $cite );
     480                                        self::$pending[] = array( $property, $value, $cite );
     481                                        continue;
     482                                }
     483                                $properties[ $property ][ $i ] = $cite;
     484                        }
     485                }
     486                return $properties;
     487        }
     488
     489        /**
     490         * Fetches and parses a URL into an mf2 citation.
     491         *
     492         * @since 4.0.0
     493         *
     494         * @access public
     495         *
     496         * @param string $url  URL of the cited page.
     497         * @param array  $args {
     498         *     Optional. Limits for this parse.
     499         *
     500         *     @type int $timeout      HTTP timeout in seconds. Default: Parse This's own (15).
     501         *     @type int $max_requests Follow-up requests allowed. Default: Parse This's own (10).
     502         * }
     503         * @return array|WP_Error mf2 citation, or WP_Error if the page could not be fetched or parsed.
     504         */
     505        public static function parse_citation( $url, $args = array() ) {
     506                $timeout       = isset( $args['timeout'] ) ? (int) $args['timeout'] : 0;
     507                $max_requests  = isset( $args['max_requests'] ) ? (int) $args['max_requests'] : null;
     508                $limit_timeout = static function ( $request ) use ( $timeout ) {
     509                        $request['timeout'] = isset( $request['timeout'] ) ? min( (float) $request['timeout'], $timeout ) : $timeout;
     510                        return $request;
     511                };
     512                $limit_budget  = static function () use ( $max_requests ) {
     513                        return $max_requests;
     514                };
     515                if ( $timeout > 0 ) {
     516                        add_filter( 'http_request_args', $limit_timeout, 99 );
     517                }
     518                if ( null !== $max_requests ) {
     519                        add_filter( 'parse_this_max_requests', $limit_budget, 99 );
     520                }
     521                try {
     522                        $parse = new \ParseThis\Parser( $url );
     523                        $fetch = $parse->fetch();
     524                        if ( is_wp_error( $fetch ) ) {
     525                                return $fetch;
     526                        }
     527                        // Keep nested objects in place rather than moving them to refs.
     528                        $parse->parse( array( 'references' => false ) );
     529                        $jf2 = $parse->get();
     530                } finally {
     531                        remove_filter( 'http_request_args', $limit_timeout, 99 );
     532                        remove_filter( 'parse_this_max_requests', $limit_budget, 99 );
     533                }
     534                if ( ! is_array( $jf2 ) || empty( $jf2 ) ) {
     535                        return new WP_Error( 'empty', __( 'Nothing could be parsed from the URL.', 'indieweb-post-kinds' ) );
     536                }
     537                // Content Parse This did not recognize is returned whole under raw.
     538                unset( $jf2['raw'] );
     539                $type = isset( $jf2['type'] ) ? $jf2['type'] : '';
     540                // A page that parses as a feed (a home page, for example) is cited as the
     541                // page itself, not its entries.
     542                if ( 'feed' === $type ) {
     543                        $jf2 = array_intersect_key( $jf2, array_flip( array( 'name', 'url', 'author', 'summary', 'photo' ) ) );
     544                }
     545                $jf2 = self::clean_citation( $jf2 );
     546                // Entries and feeds become citations. Other types, such as an h-event for
     547                // an RSVP, are kept.
     548                $jf2['type'] = in_array( $type, array( '', 'entry', 'feed' ), true ) ? 'cite' : $type;
     549                return \ParseThis\jf2_to_mf2( $jf2 );
     550        }
     551
     552        /**
     553         * Whether to enrich citation URLs in Micropub requests.
     554         *
     555         * @since 4.0.0
     556         *
     557         * @param array|null $input Micropub request, or null outside a request.
     558         * @return bool
     559         */
     560        public static function enrichment_enabled( $input = null ) {
     561                /**
     562                 * Filters whether Post Kinds enriches citation URLs in Micropub requests
     563                 * (in-reply-to, like-of, and so on) with data parsed from the cited page.
     564                 *
     565                 * Return false to store citations exactly as the client sent them.
     566                 *
     567                 * @since 4.0.0
     568                 *
     569                 * @param bool       $enabled Whether to enrich citations. Default true.
     570                 * @param array|null $input   Micropub request, or null when a background retry runs.
     571                 */
     572                return (bool) apply_filters( 'post_kinds_micropub_enrich', true, $input );
     573        }
     574
     575        /**
     576         * Returns the limits for parsing citations during a Micropub request.
     577         *
     578         * @since 4.0.0
     579         *
     580         * @return array Arguments for parse_citation().
     581         */
     582        public static function request_parse_args() {
     583                /**
     584                 * Filters the HTTP timeout, in seconds, for parsing a citation while a
     585                 * Micropub client waits. Citations that time out are retried in the
     586                 * background.
     587                 *
     588                 * @since 4.0.0
     589                 *
     590                 * @param int $timeout Timeout in seconds. Default 5.
     591                 */
     592                $timeout = (int) apply_filters( 'post_kinds_micropub_parse_timeout', 5 );
     593                return array(
     594                        'timeout'      => max( 1, $timeout ),
     595                        'max_requests' => 0,
     596                );
     597        }
     598
     599        /**
     600         * Classifies a parse failure to decide whether to retry it.
     601         *
     602         * @since 4.0.0
     603         *
     604         * @param WP_Error $error Parse failure.
     605         * @return string One of:
     606         *                'temporary'   A connection problem or timeout, a server error or rate limiting.
     607         *                'unavailable' The page was missing or refused (HTTP 404, 401 or 403), which
     608         *                              may be an outage or a configuration problem on the cited site.
     609         *                'permanent'   Anything else, such as content that cannot be parsed.
     610         */
     611        public static function failure_type( $error ) {
     612                if ( 'http_request_failed' === $error->get_error_code() ) {
     613                        return 'temporary';
     614                }
     615                $code = self::failure_response_code( $error );
     616                if ( 429 === $code || $code >= 500 ) {
     617                        return 'temporary';
     618                }
     619                if ( in_array( $code, array( 401, 403, 404 ), true ) ) {
     620                        return 'unavailable';
     621                }
     622                return 'permanent';
     623        }
     624
     625        /**
     626         * Returns the HTTP status code of a parse failure, if there was one.
     627         *
     628         * @since 4.0.0
     629         *
     630         * @param WP_Error $error Parse failure.
     631         * @return int HTTP status code, or 0.
     632         */
     633        private static function failure_response_code( $error ) {
     634                $data = $error->get_error_data();
     635                return is_array( $data ) && isset( $data['response_code'] ) ? (int) $data['response_code'] : 0;
     636        }
     637
     638        /**
     639         * Returns how long to wait before retrying a failed citation, if at all.
     640         *
     641         * Temporary failures are retried after a minute, then after 15 minutes, up
     642         * to ENRICH_ATTEMPTS attempts in all. A missing or refused page is retried
     643         * once, after some hours, in case the cited site was down or misconfigured.
     644         * Other failures are not retried.
     645         *
     646         * @since 4.0.0
     647         *
     648         * @param int      $post_id Post ID.
     649         * @param string   $url     Cited URL.
     650         * @param WP_Error $error   The failure.
     651         * @param int      $attempt Which attempt failed.
     652         * @return int Seconds to wait, or 0 for no retry.
     653         */
     654        public static function retry_delay( $post_id, $url, $error, $attempt ) {
     655                switch ( self::failure_type( $error ) ) {
     656                        case 'temporary':
     657                                if ( $attempt >= self::ENRICH_ATTEMPTS ) {
     658                                        return 0;
     659                                }
     660                                return 1 === $attempt ? MINUTE_IN_SECONDS : 15 * MINUTE_IN_SECONDS;
     661                        case 'unavailable':
     662                                // Only once per citation.
     663                                foreach ( self::get_citation_log( $post_id ) as $entry ) {
     664                                        if ( $url === $entry['url'] && 'unavailable' === $entry['type'] ) {
     665                                                return 0;
    217666                                        }
    218667                                }
    219                         }
    220                 }
    221                 return $input;
     668                                /**
     669                                 * Filters how long to wait before retrying, once, a citation whose page
     670                                 * was missing or refused (HTTP 404, 401 or 403).
     671                                 *
     672                                 * @since 4.0.0
     673                                 *
     674                                 * @param int      $delay Delay in seconds. Default 6 hours.
     675                                 * @param string   $url   Cited URL.
     676                                 * @param WP_Error $error The failure.
     677                                 */
     678                                return max( 0, (int) apply_filters( 'post_kinds_micropub_unavailable_retry_delay', 6 * HOUR_IN_SECONDS, $url, $error ) );
     679                        default:
     680                                return 0;
     681                }
     682        }
     683
     684        /**
     685         * Reports a citation that could not be parsed. Only shown when WP_DEBUG is on.
     686         *
     687         * @since 4.0.0
     688         *
     689         * @param string   $url   Cited URL.
     690         * @param WP_Error $error Parse failure.
     691         */
     692        public static function report_failure( $url, $error ) {
     693                wp_trigger_error(
     694                        __METHOD__,
     695                        sprintf( 'Could not parse %1$s for a Micropub citation: %2$s', esc_url( $url ), esc_html( $error->get_error_message() ) )
     696                );
     697        }
     698
     699        /**
     700         * Logs and, where worthwhile, schedules a retry for citations that could not
     701         * be parsed during a Micropub create or update request, once the post ID is known.
     702         *
     703         * @since 4.0.0
     704         *
     705         * @param array      $input   Micropub request.
     706         * @param array|null $wp_args Arguments of the created or updated post.
     707         */
     708        public static function schedule_enrichment( $input, $wp_args ) {
     709                $pending       = self::$pending;
     710                self::$pending = array();
     711                if ( empty( $pending ) || empty( $wp_args['ID'] ) ) {
     712                        return;
     713                }
     714                foreach ( $pending as $citation ) {
     715                        self::handle_failure( (int) $wp_args['ID'], $citation[0], $citation[1], 1, $citation[2] );
     716                }
     717        }
     718
     719        /**
     720         * Logs a failed attempt and schedules a retry if one is worthwhile.
     721         *
     722         * @since 4.0.0
     723         *
     724         * @param int      $post_id  Post ID.
     725         * @param string   $property Micropub property, such as in-reply-to.
     726         * @param string   $url      Cited URL.
     727         * @param int      $attempt  Which attempt failed.
     728         * @param WP_Error $error    The failure.
     729         */
     730        private static function handle_failure( $post_id, $property, $url, $attempt, $error ) {
     731                // Work out the delay before logging, so the log holds only earlier failures.
     732                $delay    = self::retry_delay( $post_id, $url, $error, $attempt );
     733                $retry_at = $delay ? time() + $delay : 0;
     734                self::log_outcome( $post_id, $property, $url, $attempt, $error, $retry_at );
     735                if ( $retry_at ) {
     736                        $args = array( $post_id, $property, $url, $attempt + 1 );
     737                        if ( ! wp_next_scheduled( self::ENRICH_EVENT, $args ) ) {
     738                                wp_schedule_single_event( $retry_at, self::ENRICH_EVENT, $args );
     739                        }
     740                }
     741        }
     742
     743        /**
     744         * Parses a Micropub citation in the background and stores the result.
     745         *
     746         * The citation is only replaced if the post still holds the URL as it was
     747         * stored, so later edits are not overwritten.
     748         *
     749         * @since 4.0.0
     750         *
     751         * @param int    $post_id  Post ID.
     752         * @param string $property Micropub property, such as in-reply-to.
     753         * @param string $url      Cited URL.
     754         * @param int    $attempt  Which attempt this is.
     755         */
     756        public static function enrich_citation( $post_id, $property, $url, $attempt ) {
     757                if ( ! get_post( $post_id ) || ! class_exists( '\\ParseThis\\Parser' ) || ! self::enrichment_enabled() ) {
     758                        return;
     759                }
     760                $key    = 'mf2_' . $property;
     761                $values = get_post_meta( $post_id, $key, true );
     762                $index  = is_array( $values ) ? array_search( $url, $values, true ) : false;
     763                if ( false === $index ) {
     764                        self::log_outcome( $post_id, $property, $url, $attempt, 'skipped', 0 );
     765                        return;
     766                }
     767                $cite = self::parse_citation( $url );
     768                if ( is_wp_error( $cite ) ) {
     769                        self::report_failure( $url, $cite );
     770                        self::handle_failure( $post_id, $property, $url, $attempt, $cite );
     771                        return;
     772                }
     773                $values[ $index ] = $cite;
     774                update_post_meta( $post_id, $key, $values );
     775                self::log_outcome( $post_id, $property, $url, $attempt, 'enriched', 0 );
     776        }
     777
     778        /**
     779         * Returns the log of citation parsing outcomes for a post, oldest first.
     780         *
     781         * Each entry is an array with these keys:
     782         * - time          Unix timestamp of the attempt.
     783         * - property      Micropub property, such as in-reply-to.
     784         * - url           Cited URL.
     785         * - attempt       Attempt number; 1 is the attempt during the Micropub request.
     786         * - result        'enriched', 'failed', or 'skipped' (the citation was edited since).
     787         * - type          For failures: 'temporary', 'unavailable' or 'permanent'. Otherwise empty.
     788         * - code          For failures: the WP_Error code. Otherwise empty.
     789         * - response_code For failures: the HTTP status code, or 0.
     790         * - message       For failures: the error message. Otherwise empty.
     791         * - retry         Unix timestamp of the scheduled retry, or 0 if none.
     792         *
     793         * @since 4.0.0
     794         *
     795         * @param int $post_id Post ID.
     796         * @return array[] Log entries.
     797         */
     798        public static function get_citation_log( $post_id ) {
     799                $log = get_post_meta( $post_id, self::LOG_META, true );
     800                return is_array( $log ) ? $log : array();
     801        }
     802
     803        /**
     804         * Adds an outcome to a post's citation log and updates its citation status.
     805         *
     806         * @since 4.0.0
     807         *
     808         * @param int             $post_id  Post ID.
     809         * @param string          $property Micropub property.
     810         * @param string          $url      Cited URL.
     811         * @param int             $attempt  Attempt number.
     812         * @param WP_Error|string $result   The failure, or 'enriched' or 'skipped'.
     813         * @param int             $retry_at Timestamp of the scheduled retry, or 0.
     814         */
     815        private static function log_outcome( $post_id, $property, $url, $attempt, $result, $retry_at ) {
     816                $failed = is_wp_error( $result );
     817                $entry  = array(
     818                        'time'          => time(),
     819                        'property'      => $property,
     820                        'url'           => $url,
     821                        'attempt'       => (int) $attempt,
     822                        'result'        => $failed ? 'failed' : $result,
     823                        'type'          => $failed ? self::failure_type( $result ) : '',
     824                        'code'          => $failed ? $result->get_error_code() : '',
     825                        'response_code' => $failed ? self::failure_response_code( $result ) : 0,
     826                        'message'       => $failed ? $result->get_error_message() : '',
     827                        'retry'         => (int) $retry_at,
     828                );
     829                $log    = self::get_citation_log( $post_id );
     830                $log[]  = $entry;
     831                $log    = array_slice( $log, -self::LOG_LIMIT );
     832                update_post_meta( $post_id, self::LOG_META, $log );
     833
     834                // The status reflects the latest outcome for each cited URL.
     835                $latest = array();
     836                foreach ( $log as $item ) {
     837                        $latest[ $item['url'] ] = $item;
     838                }
     839                $status = '';
     840                foreach ( $latest as $item ) {
     841                        if ( 'failed' !== $item['result'] ) {
     842                                continue;
     843                        }
     844                        if ( ! $item['retry'] ) {
     845                                $status = 'failed';
     846                                break;
     847                        }
     848                        $status = 'retrying';
     849                }
     850                if ( $status ) {
     851                        update_post_meta( $post_id, self::STATUS_META, $status );
     852                } else {
     853                        delete_post_meta( $post_id, self::STATUS_META );
     854                }
     855
     856                /**
     857                 * Fires after each attempt to parse a Micropub citation.
     858                 *
     859                 * @since 4.0.0
     860                 *
     861                 * @param array $entry   The log entry. See Kind_Plugins::get_citation_log().
     862                 * @param int   $post_id Post ID.
     863                 */
     864                do_action( 'post_kinds_citation_enrichment', $entry, $post_id );
     865        }
     866
     867        /**
     868         * Removes Parse This bookkeeping from parsed jf2, recursively.
     869         *
     870         * Keys starting with an underscore (_code, _links, _alternate,
     871         * _source_format, ...), post-type and refs are not microformats properties,
     872         * and would otherwise be stored in post meta and returned by Micropub's
     873         * q=source.
     874         *
     875         * @since 4.0.0
     876         *
     877         * @access public
     878         *
     879         * @param mixed $jf2 Parsed jf2, or any value within it.
     880         * @return mixed The value without bookkeeping keys.
     881         */
     882        public static function clean_citation( $jf2 ) {
     883                if ( ! is_array( $jf2 ) ) {
     884                        return $jf2;
     885                }
     886                foreach ( array_keys( $jf2 ) as $key ) {
     887                        if ( is_string( $key ) && ( 0 === strpos( $key, '_' ) || in_array( $key, array( 'post-type', 'refs' ), true ) ) ) {
     888                                unset( $jf2[ $key ] );
     889                        } elseif ( is_array( $jf2[ $key ] ) ) {
     890                                $jf2[ $key ] = self::clean_citation( $jf2[ $key ] );
     891                        }
     892                }
     893                return $jf2;
     894        }
     895        /**
     896         * Adds the enabled kinds to Micropub's configuration query, using the
     897         * post-types extension (Query for Supported Vocabulary), so clients only
     898         * offer the kinds this site uses.
     899         *
     900         * @since 4.0.0
     901         *
     902         * @see https://indieweb.org/Micropub-extensions#Query_for_Supported_Vocabulary
     903         *
     904         * @param array|mixed $response Micropub query response.
     905         * @param array       $input    Micropub query.
     906         * @return array|mixed The response, with post-types added for q=config.
     907         */
     908        public static function micropub_query( $response, $input ) {
     909                if ( ! is_array( $response ) || ! isset( $input['q'] ) || 'config' !== $input['q'] ) {
     910                        return $response;
     911                }
     912                $types = isset( $response['post-types'] ) && is_array( $response['post-types'] ) ? $response['post-types'] : array();
     913                $known = wp_list_pluck( $types, 'type' );
     914                foreach ( self::micropub_post_types() as $type ) {
     915                        if ( ! in_array( $type['type'], $known, true ) ) {
     916                                $types[] = $type;
     917                        }
     918                }
     919                $response['post-types'] = $types;
     920                return $response;
     921        }
     922
     923        /**
     924         * Returns the enabled kinds as Micropub post types.
     925         *
     926         * @since 4.0.0
     927         *
     928         * @return array[] Arrays with the keys type (the kind slug, which is its Post
     929         *                 Type Discovery type) and name (the kind's singular name).
     930         */
     931        public static function micropub_post_types() {
     932                // The kinds offered in the editor: those enabled in settings, plus note,
     933                // which is always available.
     934                $kinds = Kind_Config::get_termslist();
     935                array_unshift( $kinds, 'note' );
     936                $types = array();
     937                foreach ( array_unique( $kinds ) as $kind ) {
     938                        $name = Kind_Taxonomy::get_kind_info( $kind, 'singular_name' );
     939                        if ( $name ) {
     940                                $types[] = array(
     941                                        'type' => $kind,
     942                                        'name' => $name,
     943                                );
     944                        }
     945                }
     946                /**
     947                 * Filters the post types advertised to Micropub clients in q=config.
     948                 *
     949                 * @since 4.0.0
     950                 *
     951                 * @param array[] $types Arrays with the keys type and name.
     952                 */
     953                return apply_filters( 'post_kinds_micropub_post_types', $types );
    222954        }
    223955} // End Class Kind_Plugins
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-post-widget.php

    r3001401 r3738687  
    11<?php
     2/**
     3 * Kind_Post_Widget class.
     4 *
     5 * @package Post_Kinds
     6 * @since   3.1.5
     7 */
    28
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Widget that lists recent posts of selected kinds.
     13 *
     14 * @since 3.1.5
     15 */
    316class Kind_Post_Widget extends WP_Widget {
    417        /**
    518         * Register widget with WordPress.
     19         *
     20         * @since 3.1.5
    621         */
    722        public function __construct() {
    823                parent::__construct(
    9                         'Kind_Post_Widget',                // Base ID
    10                         __( 'Kind Post Widget', 'indieweb-post-kinds' ),        // Name
     24                        'Kind_Post_Widget',                // Base ID.
     25                        __( 'Kind Post Widget', 'indieweb-post-kinds' ),        // Name.
    1126                        array(
    1227                                'classname'             => 'kind_post_widget',
    … …  
    2035         * Front-end display of widget.
    2136         *
     37         * @since 3.1.5
     38         *
    2239         * @see WP_Widget::widget()
    2340         *
    … …  
    2643         */
    2744        public function widget( $args, $instance ) {
    28                 /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
    29                 $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
    30 
    31                 $kind = $instance['kind'] ?? 'note';
    32                 // phpcs:ignore
    33                 echo $args['before_widget'];
    34                 if ( ! empty( $instance['title'] ) ) {
    35                         echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore
    36                 }
    37                 $transient = get_transient( 'kind_post_widget' );
    38                 if ( false === $transient ) {
    39                                 $query = array(
    40                                         'tax_query'   => array(
    41                                                 array(
    42                                                         'taxonomy' => 'kind',
    43                                                         'field'    => 'slug',
    44                                                         'terms'    => $kind,
    45                                                 ),
     45                $kind   = $instance['kind'] ?? 'note';
     46                $number = absint( $instance['number'] ?? 5 );
     47                $posts  = get_posts(
     48                        array(
     49                                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Kinds are a taxonomy; this is the indexed way to select them.
     50                                'tax_query'           => array(
     51                                        array(
     52                                                'taxonomy' => 'kind',
     53                                                'field'    => 'slug',
     54                                                'terms'    => $kind,
    4655                                        ),
    47                                         'numberposts' => $instance['number'] ?? 5,
    48                                 );
    49                                 $posts = get_posts( $query );
     56                                ),
     57                                'numberposts'         => $number ? $number : 5,
     58                                'no_found_rows'       => true,
     59                                'ignore_sticky_posts' => true,
     60                        )
     61                );
     62                // Like core's Recent Posts widget, show nothing when there are no posts.
     63                // WP_Query caches the result in the object cache, so no transient is needed.
     64                if ( empty( $posts ) ) {
     65                        return;
    5066                }
    5167
    52                 if ( 0 === count( $posts ) ) {
    53                         return;
     68                /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
     69                $title = apply_filters( 'widget_title', $instance['title'] ?? '', $instance, $this->id_base );
     70
     71                echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper markup registered by the theme.
     72                if ( $title ) {
     73                        echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Wrapper markup is the theme's; the title is escaped by core's widget_title filter.
    5474                }
    55                 echo '<div id="kind-posts">';
    56                 if ( 0 !== count( $posts ) ) {
    57                         echo '<ul>';
    58                         foreach ( $posts as $post ) {
    59                                 printf( '<li>%1$s</li>', kind_get_the_link( $post ) ); // phpcs:ignore
    60                         }
    61                         echo '</ul>';
    62                 } else {
    63                         esc_html_e( 'No Posts Found', 'indieweb-post-kinds' );
     75                echo '<div id="kind-posts"><ul>';
     76                foreach ( $posts as $post ) {
     77                        printf( '<li>%1$s</li>', kind_get_the_link( $post ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- kind_get_the_link() escapes its parts.
    6478                }
    65                 echo '</div>';
    66                 echo $args['after_widget']; // phpcs:ignore
     79                echo '</ul></div>';
     80                echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper markup registered by the theme.
    6781        }
    6882
    6983        /**
    7084         * Sanitize widget form values as they are saved.
     85         *
     86         * @since 3.1.5
    7187         *
    7288         * @see WP_Widget::update()
    … …  
    7894         */
    7995        public function update( $new_instance, $old_instance ) {
    80                 array_walk_recursive( $new_instance, 'sanitize_text_field' );
    81                 return $new_instance;
     96                $kind   = isset( $new_instance['kind'] ) && is_string( $new_instance['kind'] ) ? sanitize_key( $new_instance['kind'] ) : '';
     97                $number = isset( $new_instance['number'] ) ? absint( $new_instance['number'] ) : 0;
     98                return array(
     99                        'title'  => isset( $new_instance['title'] ) && is_string( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '',
     100                        'kind'   => in_array( $kind, Kind_Taxonomy::get_kind_list(), true ) ? $kind : 'note',
     101                        'number' => $number > 0 ? $number : 5,
     102                );
    82103        }
    83104
    … …  
    85106        /**
    86107         * Create the form for the Widget admin
     108         *
     109         * @since 3.1.5
    87110         *
    88111         * @see WP_Widget::form()
    … …  
    93116                $instance['kind'] = ( $instance['kind'] ?? 'note' );
    94117                ?>
    95                                 <p><label for="title"><?php esc_html_e( 'Title: ', 'indieweb-post-kinds' ); ?></label>
    96                                 <input type="text" size="30" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?> id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
    97                                 value="<?php echo esc_html( $instance['title'] ?? '' ); ?>" /></p>
     118                                <p><label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title: ', 'indieweb-post-kinds' ); ?></label>
     119                                <input type="text" size="30" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
     120                                value="<?php echo esc_attr( $instance['title'] ?? '' ); ?>" /></p>
    98121                <select name="<?php echo esc_attr( $this->get_field_name( 'kind' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'kind' ) ); ?>">
    99122                <?php
    100                 $list   = get_option( 'kind_termslist', Kind_Taxonomy::get_kind_list() );
     123                $list   = Kind_Config::get_termslist();
    101124                $list[] = 'note';
     125                $list   = array_unique( $list );
    102126                foreach ( $list as $term ) {
    103                         $value = Kind_Taxonomy::get_post_kind_info( $term );
    104127                        printf(
    105128                                '<option value="%1$s" %3$s>%2$s</option>',
    106129                                esc_attr( $term ),
    107                                 Kind_Taxonomy::get_kind_info( $term, 'singular_name' ), // phpcs:ignore
    108                                 selected( $instance['kind'], $term )
     130                                esc_html( Kind_Taxonomy::get_kind_info( $term, 'singular_name' ) ),
     131                                selected( $instance['kind'], $term, false )
    109132                        );
    110                                 printf( '%1$s %2$s<br />', Kind_Taxonomy::get_icon( $term ), esc_html( $value->singular_name ) ); //phpcs:ignore
    111133                }
    112134                ?>
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-post.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Kind Post Class
     3 * Kind_Post class.
    44 *
    5  * @package Post Kinds
    6  * Assists in retrieving/saving properties from a Post. Replaces MF2_Post by only looking for Items on Demand instead of Parsing Them initially.
     5 * @package Post_Kinds
     6 * @since   3.4.0
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Reads and writes the kind properties of a post.
     13 *
     14 * Replaces MF2_Post by looking up each property on demand instead of parsing
     15 * all of them up front.
     16 *
     17 * @since 3.4.0
    718 */
    819class Kind_Post {
    920
     21        /**
     22         * Post ID.
     23         *
     24         * @since 3.4.0
     25         * @var int
     26         */
    1027        public $id;
    1128
     29        /**
     30         * Sets up the post to read and write.
     31         *
     32         * @since 3.4.0
     33         *
     34         * @param int|WP_Post|string $post Post ID, post object, or the URL of a post or attachment.
     35         */
    1236        public function __construct( $post ) {
    1337                if ( is_numeric( $post ) ) {
    … …  
    2145                                $post     = $id;
    2246                        } else {
    23                                 $id       = attachment_url_to_postid( $post );
     47                                $id       = self::get_attachment_id( $post );
    2448                                $this->id = $id;
    2549                                $post     = $id;
    … …  
    3054        }
    3155
    32         /*
    33          * Check is post is attachment.
    34          *
    35          * @param WP_Post $post Post Object.
    36          * @return boolean True if attachment.
     56        /**
     57         * Checks whether a post is an attachment.
     58         *
     59         * @since 3.4.10
     60         *
     61         * @param int|WP_Post $post Post ID or post object.
     62         * @return bool True if the post is an attachment.
    3763         */
    3864        private function is_attachment( $post ) {
    … …  
    4167
    4268
    43         /*
    44          * Returns WP_Post Object
    45          *
    46          * @return WP_Post Post Object.
    47          *
     69        /**
     70         * Returns the post object.
     71         *
     72         * @since 3.4.0
     73         *
     74         * @return WP_Post|null The post, or null if it does not exist.
    4875         */
    4976        public function get_post() {
    … …  
    5178        }
    5279
    53         /*
    54          * Returns the Post Kind. For Attachments will return the media type.
    55          *
    56          * @return string Post Kind.
    57          *
     80        /**
     81         * Returns the post kind. For an attachment, returns its media type instead.
     82         *
     83         * @since 3.4.0
     84         *
     85         * @return string|false|null The kind slug ('photo', 'video' or 'audio' for an attachment),
     86         *                           false if a post has no kind, or null for an attachment of another type.
    5887         */
    5988        public function get_kind() {
    … …  
    73102        }
    74103
    75         /*
    76          * Get Name.
    77          *
    78          * @return string Return name.
     104        /**
     105         * Returns the post's name (title).
     106         *
     107         * A title that is just the post ID, or an attachment title that is just its
     108         * file name, does not count as a name.
     109         *
     110         * @since 3.4.0
     111         *
     112         * @return string|false The name, or false if there is none.
    79113         */
    80114        public function get_name() {
    … …  
    102136        }
    103137
    104         /*
    105          * Get Content.
    106          *
    107          * @return array with HTML and Plaintext Version of Content or Summary
     138        /**
     139         * Returns the post's content or summary (excerpt).
     140         *
     141         * @since 3.4.0
     142         *
     143         * @param string $property Either 'content' or 'summary'.
     144         * @return array|false Array with the HTML ('html') and plain text ('value') versions,
     145         *                     or false if the property is empty or not supported.
    108146         */
    109147        public function get_html( $property ) {
    … …  
    124162        }
    125163
    126         /*
    127          * Get Permalink URL
    128          *
    129          * @return string $url
     164        /**
     165         * Returns the post's permalink, or the file URL for an attachment.
     166         *
     167         * @since 3.4.0
     168         *
     169         * @return string|false The URL, or false if there is none.
    130170         */
    131171        public function get_url() {
    … …  
    137177        }
    138178
    139         /*
    140          * Get Featured Image Permalink
    141          *
    142          * @return string $url
     179        /**
     180         * Returns the URL of the post's featured image.
     181         *
     182         * @since 3.4.0
     183         *
     184         * @return string|false The URL, or false if the post has no featured image.
    143185         */
    144186        public function get_featured() {
    … …  
    150192
    151193
    152         /*
    153          * Return datetime property as a DateTime Object.
    154          *
    155          * @param string $property Property You Wish to Return.
    156          * @return DateTimeImmutable Published Time in Local Timezone.
    157          *
     194        /**
     195         * Returns a date property as a date object.
     196         *
     197         * For posts other than attachments, 'published' and 'updated' come from the
     198         * post's own dates. Everything else comes from post meta.
     199         *
     200         * @since 3.4.0
     201         *
     202         * @param string $property Property to return, such as 'published' or 'start'.
     203         * @return DateTimeImmutable|false The date, or false if it is missing or not a valid date.
    158204         */
    159205        public function get_datetime_property( $property ) {
    … …  
    175221                        $datetime = $datetime[0];
    176222                }
    177                 return new DateTimeImmutable( $datetime );
    178         }
    179 
    180         /*
    181          * Returns Publication.
    182          *
    183          * @return string
    184          *
     223                return kind_safe_datetime( $datetime );
     224        }
     225
     226        /**
     227         * Returns the publication the post appeared in.
     228         *
     229         * For posts other than attachments, this is the site title.
     230         *
     231         * @since 3.4.0
     232         *
     233         * @return string|null The publication, or null if an attachment has none.
    185234         */
    186235        public function get_publication() {
    … …  
    195244        }
    196245
     246        /**
     247         * Returns the post's stored duration.
     248         *
     249         * @since 3.4.0
     250         *
     251         * @return DateInterval|false The duration, or false if it is missing or not valid.
     252         */
    197253        public function get_duration() {
    198254                $duration = get_post_meta( $this->id, 'mf2_duration', true );
    … …  
    200256                        $duration = $duration[0];
    201257                }
    202                 if ( $duration ) {
    203                         return new DateInterval( $duration );
    204                 }
    205                 return false;
    206         }
    207 
    208         /*
    209          * Return Categories which are a combination of Tags and Categories.
    210          *
    211          * @return array Array of the names of Categories and Tag names.
    212          *
     258                return kind_safe_interval( $duration );
     259        }
     260
     261        /**
     262         * Returns the names of the post's categories and tags, combined.
     263         *
     264         * 'Uncategorized' is left out.
     265         *
     266         * @since 3.4.0
     267         *
     268         * @return string[] Category and tag names.
    213269         */
    214270        public function get_categories() {
    215271                $category = array();
    216                 // Get a list of categories and extract their names
     272                // Get a list of categories and extract their names.
    217273                $post_categories = get_the_terms( $this->id, 'category' );
    218274                if ( ! empty( $post_categories ) && ! is_wp_error( $post_categories ) ) {
    … …  
    220276                }
    221277
    222                 // Get a list of tags and extract their names
     278                // Get a list of tags and extract their names.
    223279                $post_tags = get_the_terms( $this->id, 'post_tag' );
    224280                if ( ! empty( $post_tags ) && ! is_wp_error( $post_tags ) ) {
    225                         $category = array_merge( $this->category, wp_list_pluck( $post_tags, 'name' ) );
     281                        $category = array_merge( $category, wp_list_pluck( $post_tags, 'name' ) );
    226282                }
    227283                if ( in_array( 'Uncategorized', $category, true ) ) {
    … …  
    232288
    233289        /**
    234          * Is prefix in string.
    235          *
    236          * @param  string $source The source string.
    237          * @param  string $prefix The prefix you wish to check for in source.
    238          * @return boolean The result.
     290         * Checks whether a string starts with a prefix.
     291         *
     292         * @since 3.4.0
     293         *
     294         * @param string $source The source string.
     295         * @param string $prefix The prefix to look for.
     296         * @return bool True if the source starts with the prefix.
    239297         */
    240298        protected static function str_prefix( $source, $prefix ) {
    … …  
    243301
    244302        /**
    245          * Returns True if Array is Multidimensional.
    246          *
    247          * @param array $arr array.
    248          *
    249          * @return boolean result
     303         * Checks whether an array is multidimensional.
     304         *
     305         * @since 3.4.0
     306         *
     307         * @param array $arr The array to check.
     308         * @return bool True if any element is an array.
    250309         */
    251310        protected static function is_multi_array( $arr ) {
    … …  
    257316        }
    258317
     318        /**
     319         * Unwraps single-item arrays, recursively.
     320         *
     321         * @since 3.4.0
     322         *
     323         * @param mixed $value   The value to unwrap.
     324         * @param bool  $discard Optional. Whether to keep only the first item of a list
     325         *                       with more than one item. Default false.
     326         * @return mixed The value, with single-item arrays replaced by their item.
     327         */
    259328        protected function single_array( $value, $discard = false ) {
    260329                if ( ! is_array( $value ) ) {
    … …  
    274343
    275344        /**
    276          * Retrieve author
    277          *
    278          * @return boolean|array The result or false if does not exist.
     345         * Returns the post's author as an h-card.
     346         *
     347         * For an attachment, or a post with no WordPress author, this is the author
     348         * stored in post meta.
     349         *
     350         * @since 3.4.0
     351         *
     352         * @return array|string|false The author as mf2, the stored value, or false if there is none.
    279353         */
    280354        public function get_author() {
    … …  
    284358                        return ( $author ?? false );
    285359                }
    286                 // Attachments may have been uploaded by a user but may have metadata for original author
     360                // Attachments may have been uploaded by a user but may have metadata for original author.
    287361                if ( 'attachment' === get_post_type( $this->id ) ) {
    288362                        $author = get_post_meta( $post->ID, 'mf2_author', true );
    … …  
    294368                                'name'  => array( get_the_author_meta( 'display_name', $post->post_author ) ),
    295369                                'url'   => array( get_the_author_meta( 'user_url', $post->post_author ) ? get_the_author_meta( 'user_url', $post->post_author ) : get_author_posts_url( $post->post_author ) ),
    296                                 'photo' => array( get_avatar_url( $this->post_author ) ),
     370                                'photo' => array( get_avatar_url( $post->post_author ) ),
    297371                        ),
    298372                );
    299373        }
    300374
    301         /*
    302          * Return Attached Media IDs.
    303          *
    304          * @param string $type audio, video, or photo.
    305          * @return array Array of Media IDs.
    306          *
     375        /**
     376         * Returns the IDs of media attached to the post.
     377         *
     378         * @since 3.4.0
     379         *
     380         * @param string $type 'audio', 'video' or 'photo'.
     381         * @return int[]|false Attachment IDs, or false if the type is not supported.
    307382         */
    308383        public function get_attached_media( $type ) {
    … …  
    318393        }
    319394
    320         /*
    321          * Return Attached Photos.
    322          *
    323          * Looks in both attached media and the photo property.
    324          *
    325          * @param boolean $content If true then return empty if there are any images in content.
    326          * @return array Array of Media IDs.
    327          *
     395        /**
     396         * Returns the post's photos.
     397         *
     398         * Looks in attached media, the photo property and images in the content.
     399         * Returns an empty array if the post has a featured image.
     400         *
     401         * @since 3.4.0
     402         *
     403         * @param bool $content Optional. Whether to return an empty array if the content
     404         *                      has any images. Default true.
     405         * @return int[]|false Attachment IDs, or false if there are none.
    328406         */
    329407        public function get_photo( $content = true ) {
     408                if ( ! $this->get_post() ) {
     409                        return false;
     410                }
    330411                // Check if the post itself is an image attachment.
    331412                if ( wp_attachment_is( 'image', $this->id ) ) {
    … …  
    333414                }
    334415
     416                // Recorded when the post is saved; see Kind_Media_Metadata::save_post().
    335417                $content_ids = get_post_meta( $this->id, '_content_img_ids', true );
    336 
    337                 if ( false === $content_ids ) {
    338                         // Check for a gallery first.
    339                         $gallery = get_post_gallery( $this->id, false );
    340                         if ( $gallery & array_key_exists( 'ids', $gallery ) ) {
    341                                 $content_ids = explode( ',', $gallery['ids'] );
    342                         }
    343                         $content_ids = array();
    344                         $post        = $this->get_post();
    345                         $shortcode   = false;
    346                         $shortcodes  = apply_filter( 'kind_photo_shortcode_exclude', array( 'vr', '360' ) );
    347                         if ( $post->post_content ) {
    348                                 foreach ( $shortcodes as $code ) {
    349                                         if ( has_shortcode( $post->post_content, $code ) ) {
    350                                                 $shortcode = true;
    351                                         }
    352                                 }
    353                                 if ( $shortcode ) {
    354                                         $content_ids = array();
    355                                 } else {
    356                                         $content_ids = array_merge( $content_ids, Kind_Media_Metadata::get_img_from_content( $post->post_content ) );
    357                                 }
    358                                 update_post_meta( $this->id, '_content_img_ids', $content_ids );
    359                         }
    360                 }
    361418
    362419                // If there are photos in the content then end here if this is true.
    … …  
    393450        }
    394451
    395         /*
    396          * Return Attached Audio.
    397          *
    398          * Looks in both attached media and the audio property.
    399          *
    400          * @param boolean $content If true then return empty if there are any audio files in content.
    401          * @return array Array of Media IDs or URLs.
    402          *
     452        /**
     453         * Returns the post's audio.
     454         *
     455         * Looks in attached media, the audio property and audio in the content.
     456         *
     457         * @since 3.4.0
     458         *
     459         * @param bool $content Optional. Whether to return an empty array if the content
     460         *                      has any audio. Default true.
     461         * @return array|false Attachment IDs, external URLs if no attachments were found,
     462         *                     or false if there is no audio.
    403463         */
    404464        public function get_audio( $content = true ) {
    … …  
    408468                }
    409469
     470                // Recorded when the post is saved; see Kind_Media_Metadata::save_post().
    410471                $content_ids = get_post_meta( $this->id, '_content_audio_ids', true );
    411 
    412                 if ( false === $content_ids ) {
    413                         $post = get_post();
    414                         if ( $post->post_content ) {
    415                                 $content_ids = Kind_Media_Metadata::get_audio_from_content( $post->post_content );
    416                                 update_post_meta( $this->id, '_content_audio_ids', $content_ids );
    417                         }
    418                 }
    419472
    420473                // If there are ids in the content then end here if this is true.
    … …  
    431484                $audio_ids = is_array( $audios ) ? $this->get_attachments_from_urls( $audios ) : array();
    432485
    433                 // If there are ids found return them
     486                // If there are ids found return them.
    434487                if ( ! empty( $audio_ids ) || ! empty( $att_ids ) || ! empty( $content_ids ) ) {
    435                         return array_unique( array_merge( $att_ids, $this->get_attachments_from_urls( $audios ), $content_ids ) );
     488                        return array_unique( array_merge( $att_ids, $audio_ids, $content_ids ) );
    436489                }
    437490
    … …  
    444497        }
    445498
    446         /*
    447          * Return Attached Video.
    448          *
    449          * Looks in both attached media and the audio property.
    450          *
    451          * @param boolean $content If true then return empty if there are any video files in content.
    452          * @return array Array of Media IDs.
    453          *
     499        /**
     500         * Returns the post's videos.
     501         *
     502         * Looks in attached media, the video property and videos in the content.
     503         *
     504         * @since 3.4.0
     505         *
     506         * @param bool $content Optional. Whether to return an empty array if the content
     507         *                      has any videos. Default true.
     508         * @return int[]|false Attachment IDs, or false if there are none.
    454509         */
    455510        public function get_video( $content = true ) {
    … …  
    458513                        return array( $this->id );
    459514                }
     515                // Recorded when the post is saved; see Kind_Media_Metadata::save_post().
    460516                $content_ids = get_post_meta( $this->id, '_content_video_ids', true );
    461 
    462                 if ( false === $content_ids ) {
    463                         $post = get_post();
    464                         if ( $post->post_content ) {
    465                                 $content_ids = Kind_Media_Metadata::get_video_from_content( $post->post_content );
    466                                 update_post_meta( $this->id, '_content_video_ids', $content_ids );
    467                         }
    468                 }
    469517
    470518                // If there are ids in the content then end here if this is true.
    … …  
    488536        }
    489537
     538        /**
     539         * Look up the attachment ID for a URL, with caching.
     540         *
     541         * Wraps attachment_url_to_postid(), which runs an uncached query, in the
     542         * object cache. The key includes the posts last_changed value, so adding,
     543         * changing or deleting any post or attachment invalidates it. Without a
     544         * persistent object cache this still avoids repeat lookups in a request.
     545         *
     546         * @since 4.0.0
     547         *
     548         * @param string $url Media URL.
     549         * @return int Attachment ID, or 0 if there is none.
     550         */
     551        public static function get_attachment_id( $url ) {
     552                if ( ! is_string( $url ) || '' === $url ) {
     553                        return 0;
     554                }
     555                $key = 'attachment_id:' . md5( $url ) . ':' . wp_cache_get_last_changed( 'posts' );
     556                $id  = wp_cache_get( $key, 'post-kinds' );
     557                if ( false === $id ) {
     558                        $id = attachment_url_to_postid( $url );
     559                        wp_cache_set( $key, $id, 'post-kinds' );
     560                }
     561                return (int) $id;
     562        }
     563
     564        /**
     565         * Looks up the attachment IDs for media URLs.
     566         *
     567         * @since 3.4.0
     568         *
     569         * @param string|array $urls A URL, or a list of URLs, attachment IDs or arrays with a 'url' key.
     570         * @return int[] Attachment IDs found. URLs with no attachment are left out.
     571         */
    490572        public function get_attachments_from_urls( $urls ) {
    491573                if ( is_string( $urls ) ) {
    492                         $attachment = attachment_url_to_postid( $urls );
     574                        $attachment = self::get_attachment_id( $urls );
    493575                        if ( $attachment ) {
    494576                                return array( $attachment );
    … …  
    502584                                if ( is_array( $url ) ) {
    503585                                        if ( isset( $url['url'] ) ) {
    504                                                 $att_ids[] = attachment_url_to_postid( $url['url'] );
     586                                                $att_ids[] = self::get_attachment_id( $url['url'] );
    505587                                        }
    506588                                } elseif ( is_numeric( $url ) ) {
    507589                                        $att_ids[] = $url;
    508590                                } else {
    509                                         $att_ids[] = attachment_url_to_postid( $url );
     591                                        $att_ids[] = self::get_attachment_id( $url );
    510592                                }
    511593                        }
    … …  
    515597
    516598        /**
    517          * Retrieve value
    518          *
    519          * @param  string $key The key to retrieve.
    520          * @param  boolean $single Whether to return a a single value or array if there is only one value.
    521          * @return mixed The result or false if does not exist.
     599         * Returns a property of the post.
     600         *
     601         * @since 3.4.0
     602         *
     603         * @param string $key    The property to return.
     604         * @param bool   $single Optional. Whether to unwrap a single value from its array. Default true.
     605         * @return mixed The value, or false if the key is empty.
    522606         */
    523607        public function get( $key, $single = true ) {
    … …  
    559643        }
    560644
     645        /**
     646         * Returns the post's citation, or one property of it.
     647         *
     648         * For an attachment, the citation is built from the attachment itself.
     649         * Citations stored as jf2 are returned as mf2.
     650         *
     651         * @since 3.4.0
     652         *
     653         * @param string|null $key Optional. Citation property to return. Default null, for the whole citation.
     654         * @return mixed The citation as mf2 or a string, the property, or false if there is none.
     655         */
    561656        public function get_cite( $key = null ) {
    562657                if ( 'attachment' === get_post_type( $this->id ) ) {
    … …  
    569664                                $duration = date_interval_to_iso8601( $duration );
    570665                        }
    571                         $cite = jf2_to_mf2(
     666                        $cite = \ParseThis\jf2_to_mf2(
    572667                                array_filter(
    573668                                        array(
    … …  
    604699                                        $cite = $cite[0];
    605700                                }
    606                                 $this->delete( 'cite' );
    607                         }
    608                 }
    609 
    610                 // If this is formatted as JF2 try to convert it to MF2 and update.
    611                 if ( is_array( $cite ) && ! wp_is_numeric_array( $cite ) ) {
    612                         $cite['type'] = 'cite';
    613                         $cite         = jf2_to_mf2( $cite );
    614                         $property     = Kind_Taxonomy::get_kind_info( $this->get_kind(), 'property' );
    615                         $this->set( $property, $cite );
     701                        }
     702
     703                        // Citations stored as jf2 are returned as mf2.
     704                        if ( is_array( $cite ) && ! wp_is_numeric_array( $cite ) ) {
     705                                if ( ! isset( $cite['properties'] ) ) {
     706                                        if ( empty( $cite['type'] ) ) {
     707                                                $cite['type'] = $this->get_cite_type();
     708                                        }
     709                                        $cite = \ParseThis\jf2_to_mf2( $cite );
     710                                } elseif ( empty( $cite['type'] ) || ! is_array( $cite['type'] ) ) {
     711                                        // Reads before 4.0.0 stored the type as 'cite'.
     712                                        $cite['type'] = array( 'h-' . $this->get_cite_type() );
     713                                }
     714                        }
    616715                }
    617716
    … …  
    649748        }
    650749
    651         /* Returns a normalized cite with all possible parameters present to reduce isset checks and to ensure everything is formatted correctly
    652          * For Display and Metabox purposes only.
     750        /**
     751         * Returns the jf2 type of the post's citation, based on its kind.
     752         *
     753         * Matches the type Kind_Metabox::save_post() stores: a checkin cites a
     754         * place (card), eat and drink cite food, and every other kind a citation.
     755         *
     756         * @since 4.0.0
     757         *
     758         * @return string The jf2 type: 'card', 'food' or 'cite'.
     759         */
     760        public function get_cite_type() {
     761                $kind = $this->get_kind();
     762                if ( 'checkin' === $kind ) {
     763                        return 'card';
     764                }
     765                if ( in_array( $kind, array( 'drink', 'eat' ), true ) ) {
     766                        return 'food';
     767                }
     768                return 'cite';
     769        }
     770
     771        /**
     772         * Returns a citation as jf2 with every key present and formatted for display.
     773         *
     774         * Missing keys are set to empty strings, which saves isset checks. For
     775         * display and the metabox only.
     776         *
     777         * 'authors' lists every author as a card with name, url and photo. 'author'
     778         * is the single author, or for several authors one card whose fields hold
     779         * each author's value in turn, separated by semicolons, as the metabox
     780         * shows them.
     781         *
     782         * @since 3.4.9
     783         * @since 4.0.0 Added 'authors'. 'author' joins several authors' photos too.
     784         *
     785         * @param array|string|false $cite The citation, as mf2, jf2 or a URL or name.
     786         * @return array The normalized citation.
    653787         */
    654788        public function normalize_cite( $cite ) {
    655                 // Ensures that an empty string is always present in the cite
     789                // Ensures that an empty string is always present in the cite.
    656790                $author_defaults = array(
    657791                        'type'  => 'card',
    … …  
    670804                        'summary'     => '',
    671805                        'author'      => $author_defaults,
     806                        'authors'     => array(),
    672807                        'category'    => '',
    673808                );
    … …  
    680815                        $cite = $cite[0];
    681816                }
    682                 if ( Parse_This_MF2_Utils::is_microformat( $cite ) ) {
    683                         $cite = mf2_to_jf2( $cite );
     817                if ( \ParseThis\MF2_Utils::is_microformat( $cite ) ) {
     818                        $cite = \ParseThis\mf2_to_jf2( $cite );
    684819                }
    685820
    … …  
    698833                }
    699834
    700                 if ( wp_is_numeric_array( $cite['author'] ) ) {
    701                         if ( 1 === count( $cite['author'] ) ) {
    702                                 $cite['author'] = array_pop( $cite['author'] );
    703                         }
    704                 }
    705 
    706                 if ( is_string( $cite['author'] ) ) {
    707                         $cite['author'] = wp_http_validate_url( $cite['author'] ) ? array( 'url' => $cite['author'] ) : array( 'name' => $cite['author'] );
    708                 }
    709 
    710                 $cite['author'] = wp_parse_args( $cite['author'], $author_defaults );
     835                $cite['authors'] = self::normalize_authors( $cite['author'] );
     836                if ( 1 === count( $cite['authors'] ) ) {
     837                        $cite['author'] = $cite['authors'][0];
     838                } else {
     839                        $cite['author'] = $author_defaults;
     840                        foreach ( array( 'name', 'url', 'photo' ) as $key ) {
     841                                $values = wp_list_pluck( $cite['authors'], $key );
     842                                if ( array_filter( $values ) ) {
     843                                        $cite['author'][ $key ] = implode( '; ', $values );
     844                                }
     845                        }
     846                }
    711847
    712848                if ( is_array( $cite['publication'] ) ) {
    713849                        $cite['publication'] = $cite['publication']['name'];
    714                 }
    715 
    716                 if ( is_array( $cite['author']['name'] ) ) {
    717                         $cite['author']['name'] = implode( ';', $cite['author']['name'] );
    718                 }
    719                 if ( is_array( $cite['author']['url'] ) ) {
    720                         $cite['author']['url'] = implode( ';', $cite['author']['url'] );
    721850                }
    722851
    … …  
    733862        }
    734863
     864
     865        /**
     866         * Returns a citation's authors as a list of cards.
     867         *
     868         * Accepts one author or a list, each as a jf2 or mf2 h-card, a URL or a
     869         * name. Before 4.0.0 the metabox saved several authors as one card whose
     870         * name, url and photo were lists; the values are paired up by position.
     871         *
     872         * @since 4.0.0
     873         *
     874         * @param mixed $author The author or authors.
     875         * @return array[] Cards with string type, name, url and photo, leaving out empty ones.
     876         */
     877        public static function normalize_authors( $author ) {
     878                if ( wp_is_numeric_array( $author ) ) {
     879                        $authors = array();
     880                        foreach ( $author as $item ) {
     881                                $authors = array_merge( $authors, self::normalize_authors( $item ) );
     882                        }
     883                        return $authors;
     884                }
     885                if ( is_string( $author ) ) {
     886                        $author = wp_http_validate_url( $author ) ? array( 'url' => $author ) : array( 'name' => $author );
     887                }
     888                if ( ! is_array( $author ) ) {
     889                        return array();
     890                }
     891                if ( \ParseThis\MF2_Utils::is_microformat( $author ) ) {
     892                        $author = \ParseThis\mf2_to_jf2( $author );
     893                }
     894
     895                $fields = array();
     896                foreach ( array( 'name', 'url', 'photo' ) as $key ) {
     897                        $value = isset( $author[ $key ] ) ? $author[ $key ] : '';
     898                        if ( is_array( $value ) && isset( $value['value'] ) ) {
     899                                // A jf2 photo with alt text.
     900                                $value = $value['value'];
     901                        }
     902                        $fields[ $key ] = array_values( array_filter( (array) $value, 'is_scalar' ) );
     903                }
     904
     905                $authors = array();
     906                $count   = max( array_map( 'count', $fields ) );
     907                for ( $i = 0; $i < $count; $i++ ) {
     908                        $card = array( 'type' => 'card' );
     909                        foreach ( $fields as $key => $values ) {
     910                                $card[ $key ] = isset( $values[ $i ] ) ? trim( (string) $values[ $i ] ) : '';
     911                        }
     912                        if ( '' !== $card['name'] . $card['url'] . $card['photo'] ) {
     913                                $authors[] = $card;
     914                        }
     915                }
     916                return $authors;
     917        }
     918        /**
     919         * Stores a date property.
     920         *
     921         * For posts other than attachments, 'published' and 'updated' set the
     922         * post's own dates. Everything else is stored in post meta.
     923         *
     924         * @since 3.4.0
     925         *
     926         * @param string $key   Property to set, such as 'published' or 'start'.
     927         * @param mixed  $value A date, in any form kind_safe_datetime() accepts.
     928         * @return int|bool|WP_Error The result of updating the post or its meta,
     929         *                           or false if the value is not a valid date.
     930         */
    735931        public function set_datetime_property( $key, $value ) {
    736932                // In an attachment the post date properties reflect when the item was uploaded not when the piece was created.
    737                 if ( ! $value instanceof DateTime ) {
    738                         $value = new DateTime( $value );
    739                 }
     933                // A mutable copy, as the timezone is changed below.
     934                $value = kind_safe_datetime( $value, null, false );
    740935                if ( ! $value ) {
    741936                        return false;
    … …  
    755950        }
    756951
     952        /**
     953         * Stores the post's duration as an ISO 8601 duration.
     954         *
     955         * @since 3.4.0
     956         *
     957         * @param mixed $value A duration, in any form kind_safe_interval() accepts.
     958         * @return int|bool The result of updating the meta, or false if the value is not a valid duration.
     959         */
    757960        public function set_duration( $value ) {
    758                 if ( ! $value instanceof DateInterval ) {
    759                         $value = new DateInterval( $value );
    760                 }
     961                $value = kind_safe_interval( $value );
    761962                if ( ! $value ) {
    762963                        return false;
    … …  
    769970
    770971        /**
    771          * Set author
    772          *
    773          * @param array $value Author microformat.
    774          * @return boolean|WP_Error
     972         * Stores the author of an attachment.
     973         *
     974         * Other posts use their WordPress author, so nothing is stored for them.
     975         *
     976         * @since 3.4.0
     977         *
     978         * @param array $value Author, as mf2.
     979         * @return int|bool|null The result of updating the meta, or null if the post is not an attachment.
    775980         */
    776981        public function set_author( $value ) {
    777                 // Attachments may have been uploaded by a user but may have metadata for original author
     982                // Attachments may have been uploaded by a user but may have metadata for original author.
    778983                if ( 'attachment' === get_post_type( $this->id ) ) {
    779984                        return update_post_meta( $this->id, 'mf2_author', $value );
    … …  
    781986        }
    782987
     988        /**
     989         * Stores a property, or several.
     990         *
     991         * @since 3.4.0
     992         *
     993         * @param string|array $key   Property to set, or an array of property => value pairs.
     994         * @param mixed        $value Optional. The value. Default null.
     995         * @return mixed The result of storing the value. True when setting an array,
     996         *               and null if the key or value is empty.
     997         */
    783998        public function set( $key, $value = null ) {
    784999                if ( is_array( $key ) ) {
    … …  
    8041019                        case 'featured':
    8051020                                if ( wp_http_validate_url( $value ) ) {
    806                                         $featured = attachment_url_to_postid( $value );
     1021                                        $featured = self::get_attachment_id( $value );
    8071022                                        if ( $featured ) {
    8081023                                                $value = $featured;
    … …  
    8321047                                return wp_update_post( $args, true );
    8331048                        case 'audio':
    834                                 /* All media is handled identically.
    835                                 */
     1049                                // All media is handled identically.
    8361050                        case 'video':
    837                                 /* All Media is handled identically.
    838                                 */
     1051                                // All media is handled identically.
    8391052                        case 'photo':
    840                                 if ( Parse_This_MF2::is_microformat( $value ) ) {
    841                                         $url = Parse_This_MF2::get_plaintext( $value, 'url' );
    842                                         $id  = attachment_url_to_postid( $url );
     1053                                if ( \ParseThis\MF2_Utils::is_microformat( $value ) ) {
     1054                                        $url = \ParseThis\MF2_Utils::get_plaintext( $value, 'url' );
     1055                                        $id  = self::get_attachment_id( $url );
    8431056                                        if ( $id ) {
    844                                                 $value = mf2_to_jf2( $value );
     1057                                                $value = \ParseThis\mf2_to_jf2( $value );
    8451058                                                unset( $value['type'] );
    8461059                                                $attachment = new Kind_Post( $id );
    … …  
    8521065                                        return update_post_meta( $this->id, 'mf2_' . $key, array( $url ) );
    8531066                                }
    854                                 /* If it is not a microformat handle as default.
    855                                 */
     1067                                // If it is not a microformat handle as default.
    8561068                        default:
    8571069                                return update_post_meta( $this->id, 'mf2_' . $key, $value );
    … …  
    8591071        }
    8601072
     1073        /**
     1074         * Deletes a property stored in post meta.
     1075         *
     1076         * @since 3.4.0
     1077         *
     1078         * @param string $key Property to delete.
     1079         * @return bool True on success, false on failure.
     1080         */
    8611081        public function delete( $key ) {
    8621082                return delete_post_meta( $this->id, 'mf2_' . $key );
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-taxonomy.php

    r3011061 r3738687  
    11<?php
    2 
    32/**
    4  * Post Kind Taxonomy Class
     3 * Kind_Taxonomy class.
    54 *
    6  * Registers the taxonomy and sets its behavior.
     5 * @package Post_Kinds
     6 * @since   2.0.2
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Registers the kind taxonomy and sets its behavior.
    713 *
    8  * @package Post Kinds
     14 * @since 2.0.2
    915 */
    1016final class Kind_Taxonomy {
    11         private static $kinds = array(); // Store a Post_Kind class which is a definition of a specific kind
    12 
     17        /**
     18         * Registered kinds, keyed by slug.
     19         *
     20         * @since 3.1.0
     21         * @var Post_Kind[]
     22         */
     23        private static $kinds = array();
     24
     25        /**
     26         * Whether the built-in kinds have been registered.
     27         *
     28         * @since 4.0.0
     29         * @var bool
     30         */
     31        private static $builtins_loaded = false;
     32
     33        /**
     34         * Loads the built-in kinds and adds the taxonomy's hooks.
     35         *
     36         * @since 2.0.2
     37         */
    1338        public static function init() {
    14                 $wp_version = get_bloginfo( 'version' );
    15                 require_once plugin_dir_path( __FILE__ ) . '/register-kinds.php';
     39                self::load_kinds();
    1640
    1741                // Add the Correct Archive Title to Kind Archives.
    18                 if ( version_compare( $wp_version, '5.5', '>' ) ) {
    19                         add_filter( 'get_the_archive_title', array( self::class, 'kind_archive_title' ), 10 );
    20                 } else {
    21                         add_filter( 'get_the_archive_title', array( self::class, 'kind_archive_title' ), 10, 3 );
    22                 }
     42                add_filter( 'get_the_archive_title', array( self::class, 'kind_archive_title' ), 10 );
    2343
    2444                add_filter( 'get_the_archive_title_prefix', array( self::class, 'kind_archive_prefix' ), 10 );
    … …  
    2646                add_filter( 'document_title_parts', array( self::class, 'document_title_parts' ), 10 );
    2747
     48                // Keep the taxonomy's default term in step with the Default Kind setting.
     49                add_action( 'add_option_kind_default', array( self::class, 'sync_default_term' ) );
     50                add_action( 'update_option_kind_default', array( self::class, 'sync_default_term' ) );
     51
    2852                // Add Kind Permalinks.
    2953                add_filter( 'post_link', array( self::class, 'kind_permalink' ), 10, 3 );
    3054                add_filter( 'post_type_link', array( self::class, 'kind_permalink' ), 10, 3 );
    3155
    32                 // Query Variable to Exclude Kinds from Feed
     56                // Query Variable to Exclude Kinds from Feed.
    3357                add_filter( 'query_vars', array( self::class, 'query_vars' ) );
    3458                add_action( 'pre_get_posts', array( self::class, 'kind_filter_query' ) );
    … …  
    3761                add_action( 'pre_get_posts', array( self::class, 'kind_firehose_query' ), 99 );
    3862
    39                 // Add Dropdown
     63                // Add Dropdown.
    4064                add_action( 'restrict_manage_posts', array( self::class, 'kind_dropdown' ), 10, 2 );
    4165
    … …  
    4367                add_filter( 'webmention_links', array( self::class, 'webmention_links' ), 11, 2 );
    4468
    45                 // Add Links to Enclosures if Appropriate
     69                // Add Links to Enclosures if Appropriate.
    4670                add_filter( 'enclosure_links', array( self::class, 'enclosure_links' ), 11, 2 );
    4771
    … …  
    5175                // Trigger Webmention on Change in Post Status.
    5276                add_filter( 'transition_post_status', array( self::class, 'transition' ), 10, 3 );
    53                 // On Post Save Set Post Format
     77                // On Post Save Set Post Format.
    5478                add_action( 'save_post', array( self::class, 'post_formats' ), 99, 3 );
    5579
    56                 // Create hook triggered by change of kind
     80                // Create hook triggered by change of kind.
    5781                add_action( 'set_object_terms', array( self::class, 'set_object_terms' ), 10, 6 );
    5882
    … …  
    6993        }
    7094
    71         /** Template Redirect
    72          *
     95        /**
     96         * Uses the kind photos template for photo archives, if the theme has one.
     97         *
     98         * @since 3.4.14
     99         *
     100         * @param string $template Path of the template to include.
     101         * @return string Path of the template to include.
    73102         */
    74103        public static function template_include( $template ) {
    75104                global $wp_query;
    76105
    77                 // Only use this template for archive pages
     106                // Only use this template for archive pages.
    78107                if ( is_singular() ) {
    79108                        return $template;
    … …  
    92121        /**
    93122         * Register the Route.
     123         *
     124         * @since 3.3.4
    94125         */
    95126        public static function register_routes() {
    … …  
    107138                                                ),
    108139                                        ),
     140                                        // For editor tools: only users who can write posts need it.
    109141                                        'permission_callback' => function () {
    110                                                 return current_user_can( 'read' );
     142                                                return current_user_can( 'edit_posts' );
    111143                                        },
    112144                                ),
    … …  
    115147        }
    116148
     149        /**
     150         * Returns a kind's registration info for the post-kinds/1.0/fields route.
     151         *
     152         * @since 3.3.4
     153         *
     154         * @param WP_REST_Request $request Request, with the kind slug (or all) in kind.
     155         * @return Post_Kind|Post_Kind[]|WP_Error The kind, all kinds, or an error for an unknown kind.
     156         */
    117157        public static function read( $request ) {
    118158                $kind = $request->get_param( 'kind' );
    119                 return self::get_kind_info( $kind, 'all' );
     159                $info = self::get_kind_info( $kind, 'all' );
     160                if ( false === $info ) {
     161                        return new WP_Error( 'rest_post_kind_not_found', __( 'Unknown post kind.', 'indieweb-post-kinds' ), array( 'status' => 404 ) );
     162                }
     163                return $info;
    120164        }
    121165
    … …  
    123167        /**
    124168         * Add our query variables to query_vars list.
     169         *
     170         * @since 3.2.4
    125171         *
    126172         * @access public
    … …  
    140186         * Filter the query for our post kinds.
    141187         *
    142          * @access public
    143          *
    144          * @param $query
     188         * @since 3.4.14
     189         *
     190         * @access public
     191         *
     192         * @param WP_Query $query The query.
     193         * @return WP_Query The query.
    145194         */
    146195        public static function kind_photo_filter( $query ) {
    147                 // check if the user is requesting an admin page
     196                // check if the user is requesting an admin page.
    148197                if ( is_admin() || ! $query->is_main_query() ) {
    149198                        return $query;
    150199                }
    151200                $photos = get_query_var( 'kind_photos' );
    152                 // Return if  not set
     201                // Return if  not set.
    153202                if ( $photos ) {
    154203                        $query->set( 'posts_per_page', 30 );
    … …  
    156205                        $query->is_comment_feed = false;
    157206                        $query->is_home         = false;
     207                        // Set for posts with images in their content or a photo property; see Kind_Media_Metadata::update_photo_flag().
    158208                        $query->set(
    159209                                'meta_query',
    160210                                array(
    161                                         'relation' => 'OR',
    162211                                        array(
    163                                                 'key'     => '_content_img_ids',
    164                                                 'compare' => 'EXISTS',
    165                                         ),
    166                                         array(
    167                                                 'key'     => 'mf2_photo',
     212                                                'key'     => '_kind_has_photo',
    168213                                                'compare' => 'EXISTS',
    169214                                        ),
    … …  
    177222         * Filter the query for our post kinds.
    178223         *
    179          * @access public
    180          *
    181          * @param $query
     224         * @since 3.2.4
     225         *
     226         * @access public
     227         *
     228         * @param WP_Query $query The query.
     229         * @return WP_Query The query.
    182230         */
    183231        public static function kind_filter_query( $query ) {
    184                 // check if the user is requesting an admin page
     232                // check if the user is requesting an admin page.
    185233                if ( is_admin() || ! $query->is_main_query() ) {
    186234                        return $query;
    187235                }
    188236                $exclude = get_query_var( 'exclude' );
    189                 // Return if both are not set
     237                // Return if both are not set.
    190238                if ( ! taxonomy_exists( $exclude ) ) {
    191239                        return $query;
    … …  
    214262         * Filter the main query for post kinds.
    215263         *
    216          * @access public
    217          *
    218          * @param $query
     264         * @since 3.6.0
     265         *
     266         * @access public
     267         *
     268         * @param WP_Query $query The query.
     269         * @return WP_Query The query.
    219270         */
    220271        public static function kind_firehose_query( $query ) {
    221272
    222                 // check if the user is requesting an admin page
     273                // check if the user is requesting an admin page.
    223274                if ( is_admin() || ! $query->is_main_query() ) {
    224275                        return $query;
    … …  
    246297                }
    247298
    248                 $query->set(
    249                         'tax_query',
     299                $tax_query = array(
    250300                        array(
    251                                 'relation' => 'OR',
    252                                 array(
    253                                         'taxonomy' => 'kind',
    254                                         'field'    => 'slug',
    255                                         'terms'    => $firehose,
    256                                         'operator' => 'IN',
    257                                 ),
    258                                 array(
    259                                         'taxonomy' => 'kind',
    260                                         'operator' => 'NOT EXISTS',
    261                                 ),
    262                         )
    263                 );
     301                                'taxonomy' => 'kind',
     302                                'field'    => 'slug',
     303                                'terms'    => $firehose,
     304                                'operator' => 'IN',
     305                        ),
     306                );
     307                // Posts without a kind are shown too. The upgrade routine gives them one,
     308                // so the costly NOT EXISTS clause is only needed until it completes.
     309                if ( Kind_Upgrade::is_pending() ) {
     310                        $tax_query['relation'] = 'OR';
     311                        $tax_query[]           = array(
     312                                'taxonomy' => 'kind',
     313                                'operator' => 'NOT EXISTS',
     314                        );
     315                }
     316                $query->set( 'tax_query', $tax_query );
    264317                return $query;
    265318        }
    … …  
    268321         * Allows for some pre-defined aliases
    269322         *
    270          * @access public
    271          *
    272          * @param $query
     323         * @since 3.4.19
     324         *
     325         * @access public
     326         *
     327         * @param WP_Query $query The query.
     328         * @return WP_Query The query.
    273329         */
    274330        public static function kind_alias_filter( $query ) {
    … …  
    292348        /**
    293349         * Register our REST API endpoint for post kinds.
     350         *
     351         * @since 3.1.0
    294352         *
    295353         * @access public
    … …  
    300358                        'kind',
    301359                        array(
    302                                 'get_callback'    => array( self::class, 'get_post_kind_slug' ),
     360                                'get_callback'    => array( self::class, 'get_rest_post_kind' ),
    303361                                'update_callback' => array( self::class, 'set_rest_post_kind' ),
    304362                                'schema'          => array(
    305                                         'kind' => __( 'Post Kind', 'indieweb-post-kinds' ),
    306                                         'type' => 'string',
     363                                        'description' => __( 'The post kind, or false if the post has none.', 'indieweb-post-kinds' ),
     364                                        // False is only returned, never accepted: the enum allows kind slugs alone.
     365                                        'type'        => array( 'string', 'boolean' ),
     366                                        'enum'        => self::get_kind_list(),
     367                                        'context'     => array( 'view', 'edit' ),
    307368                                ),
    308369                        )
    … …  
    312373        /**
    313374         * Filter template hierarchy to include our template files.
     375         *
     376         * @since 3.1.2
    314377         *
    315378         * @access public
    … …  
    332395         * Generate a sample permalink for a post.
    333396         *
     397         * @since 3.2.2
     398         *
    334399         * @access public
    335400         *
    … …  
    348413                if ( is_numeric( $permalink[1] ) && $post_id === (int) $permalink[1] ) {
    349414                        $excerpt = self::get_excerpt( $post );
    350                         $excerpt = sanitize_title( mb_strimwidth( wp_strip_all_tags( $excerpt ), 0, 40 ) ); // phpcs:ignore
     415                        $excerpt = sanitize_title( mb_strimwidth( wp_strip_all_tags( $excerpt ), 0, 40 ) );
    351416                        if ( ! empty( $excerpt ) ) {
    352417                                $permalink[1] = wp_unique_post_slug( $excerpt, $post_id, $post->post_status, $post->post_type, $post->post_parent );
    … …  
    359424         * Generate a post title.
    360425         *
    361          * @access public
    362          *
    363          * @param int!WP_Post    $post Post ID or Post Object.
    364          * @return string
     426         * @since 3.3.3
     427         *
     428         * @access public
     429         *
     430         * @param int|WP_Post $post   Post ID or post object.
     431         * @param int         $length Optional. Maximum length of the title. Default 40.
     432         * @return string|null The title, or null if the post does not exist.
    365433         */
    366434        public static function generate_title( $post, $length = 40 ) {
    … …  
    374442                        $kind_post = new Kind_Post( $post );
    375443                        $cite      = $kind_post->get_cite();
    376                         if ( Parse_This_MF2::is_microformat( $cite ) ) {
     444                        // Citations are stored as a list; use the first.
     445                        if ( wp_is_numeric_array( $cite ) && 1 === count( $cite ) ) {
     446                                $cite = $cite[0];
     447                        }
     448                        if ( class_exists( '\\ParseThis\\MF2_Utils' ) && \ParseThis\MF2_Utils::is_microformat( $cite ) ) {
    377449                                if ( array_key_exists( 'name', $cite['properties'] ) ) {
    378450                                        $excerpt = $cite['properties']['name'];
    … …  
    383455                        }
    384456                }
    385                 return mb_strimwidth( $excerpt, 0, $length, '...' ); // phpcs:ignore
     457                return mb_strimwidth( $excerpt, 0, $length, '...' );
    386458        }
    387459
    … …  
    389461         * Filter the post title. Add a symbol at the end if generated title.
    390462         *
    391          * @access public
    392          *
    393          * @param string $title   Current post title
    394          * @param int    $post_id Post ID
     463         * @since 3.0.9
     464         *
     465         * @access public
     466         *
     467         * @param string $title   Current post title.
     468         * @param int    $post_id Post ID.
    395469         * @return string
    396470         */
    … …  
    406480                                $title = sprintf( '[%1$s] %2$s', $post_kind, self::generate_title( $post_id, 60 ) );
    407481                        }
     482                } elseif ( empty( $title ) && is_comment_feed() ) {
     483                        // Comment feed items read "Comment on <title> by <author>"; name untitled posts.
     484                        $title = self::generate_title( $post_id, 60 );
    408485                }
    409486                return $title;
    … …  
    413490         * Filter the single post title.
    414491         *
    415          * @access public
    416          *
    417          * @param string $title   Current post title
    418          * @param WP_Post    $post Post object
     492         * @since 3.3.3
     493         *
     494         * @access public
     495         *
     496         * @param string  $title Current post title.
     497         * @param WP_Post $post  Post object.
    419498         * @return string
    420499         */
    … …  
    438517         * Filter the post excerpt.
    439518         *
     519         * @since 3.2.0
     520         *
    440521         * @access public
    441522         *
    … …  
    456537         * Set our post object terms.
    457538         *
     539         * @since 3.0.8
     540         *
    458541         * @access public
    459542         *
    … …  
    461544         * @param array  $terms      Array of object terms.
    462545         * @param array  $tt_ids     Array of term taxonomy IDs.
    463          * @param string $taxonomy   Taxonomy slug
     546         * @param string $taxonomy   Taxonomy slug.
    464547         * @param bool   $append     If terms should be appended or overwrite.
    465548         * @param array  $old_tt_ids Array of original trem taxonomy IDs.
    … …  
    475558                        $new_term = $new_term instanceof WP_Term ? $new_term->slug : '';
    476559                        if ( $old_term !== $new_term ) {
    477                                 // Trigger a hook on a changed kind identifying old and new so actions can be performed
     560                                /**
     561                                 * Fires when a post's kind changes.
     562                                 *
     563                                 * @since 3.0.8
     564                                 *
     565                                 * @param int    $object_id Post ID.
     566                                 * @param string $old_term  Slug of the previous kind, or an empty string if there was none.
     567                                 * @param string $new_term  Slug of the new kind, or an empty string if the kind was removed.
     568                                 */
    478569                                do_action( 'change_kind', $object_id, $old_term, $new_term );
    479570                        }
    … …  
    483574        /**
    484575         * To Be Run on Plugin Activation.
     576         */
     577        /**
     578         * Registers the built-in kinds, once.
     579         *
     580         * Called from init() and on activation. A plugin is activated after
     581         * plugins_loaded and init have run for the request, so neither the kinds
     582         * nor the register_post_kind() function (kind-functions.php) would
     583         * otherwise be loaded when the kind terms are created.
     584         *
     585         * @since 4.0.0
     586         */
     587        public static function load_kinds() {
     588                if ( self::$builtins_loaded ) {
     589                        return;
     590                }
     591                self::$builtins_loaded = true;
     592                if ( ! function_exists( 'register_post_kind' ) ) {
     593                        require_once plugin_dir_path( __FILE__ ) . 'kind-functions.php';
     594                }
     595                require plugin_dir_path( __FILE__ ) . 'register-kinds.php';
     596        }
     597
     598        /**
     599         * Creates the kind terms on activation.
     600         *
     601         * Stops with an error if the IndieWeb Taxonomy plugin, which Post Kinds
     602         * replaces, is active.
     603         *
     604         * @since 2.0.2
    485605         */
    486606        public static function activate_kinds() {
    487607                if ( function_exists( 'iwt_plugin_notice' ) ) {
    488                         deactivate_plugins( plugin_basename( __FILE__ ) );
    489                         wp_die( 'You have Indieweb Taxonomy activated. Post Kinds replaces this plugin. Please disable Taxonomy before activating' );
    490                 }
     608                        deactivate_plugins( plugin_basename( dirname( __DIR__ ) . '/indieweb-post-kinds.php' ) );
     609                        wp_die( esc_html__( 'Post Kinds replaces the IndieWeb Taxonomy plugin. Deactivate IndieWeb Taxonomy, then activate Post Kinds.', 'indieweb-post-kinds' ) );
     610                }
     611                self::load_kinds();
    491612                self::register();
    492613                self::kind_defaultterms();
    … …  
    494615        }
    495616
     617        /**
     618         * Returns the rewrite regex for the pagination part of a URL.
     619         *
     620         * @since 3.5.0
     621         *
     622         * @return string The regex, such as 'page/?([0-9]{1,})'.
     623         */
    496624        public static function get_pagination_regex() {
    497625                global $wp_rewrite;
    … …  
    499627        }
    500628
     629        /**
     630         * Returns the rewrite regex for the feed part of a URL.
     631         *
     632         * @since 3.5.0
     633         *
     634         * @param bool $with_base Optional. Whether to start with the feed base ('feed/'). Default true.
     635         * @return string The regex, such as 'feed/(feed|rdf|rss|rss2|atom)'.
     636         */
    501637        public static function get_feed_regex( $with_base = true ) {
    502638                global $wp_rewrite;
    503                 // Build a regex to match the feed section of URLs, something like (feed|atom|rss|rss2)/?
     639                // Build a regex to match the feed section of URLs, something like (feed|atom|rss|rss2)/?.
    504640                $feedregex2 = '';
    505641                foreach ( (array) $wp_rewrite->feeds as $feed_name ) {
    … …  
    511647        }
    512648
     649        /**
     650         * Joins regex parts into a rewrite rule pattern.
     651         *
     652         * @since 3.5.0
     653         *
     654         * @param string[] $elements Parts of the pattern, in order.
     655         * @return string The parts joined by slashes and ending in '?$', or an empty string if there are none.
     656         */
    513657        public static function generate_permastruct( $elements ) {
    514658                if ( empty( $elements ) || ! is_array( $elements ) ) {
    … …  
    520664
    521665        /**
     666         * Returns the start of kind archive URLs, as in /kind/like/.
     667         *
     668         * Set as the Kind base on Settings > Permalinks.
     669         *
     670         * @since 4.0.0
     671         *
     672         * @return string The base. Default 'kind'.
     673         */
     674        public static function get_kind_base() {
     675                $base = get_option( 'kind_base', '' );
     676                $base = is_string( $base ) && '' !== $base ? $base : 'kind';
     677                /**
     678                 * Filters the start of kind archive URLs, as in /kind/like/.
     679                 *
     680                 * The Kind base on Settings > Permalinks sets it. Flush rewrite rules after
     681                 * changing this.
     682                 *
     683                 * @since 4.0.0
     684                 *
     685                 * @param string $base The base. Default the Kind base setting, or 'kind'.
     686                 */
     687                return (string) apply_filters( 'kind_base_slug', $base );
     688        }
     689
     690        /**
    522691         * Register the custom taxonomy for kinds.
     692         *
     693         * @since 2.0.2
    523694         */
    524695        public static function register() {
    … …  
    540711                        'add_or_remove_items'        => _x( 'Add or remove kinds', 'add or remove items', 'indieweb-post-kinds' ),
    541712                        'choose_from_most_used'      => _x( 'Choose from the most used kinds', 'choose most used', 'indieweb-post-kinds' ),
    542                         'not found'                  => _x( 'No kinds found', 'no kinds found', 'indieweb-post-kinds' ),
     713                        'not_found'                  => _x( 'No kinds found', 'no kinds found', 'indieweb-post-kinds' ),
    543714                        'no_terms'                   => _x( 'No kinds', 'no kinds', 'indieweb-post-kinds' ),
    544715                        'back_to_items'              => __( 'Back to Kinds', 'indieweb-post-kinds' ),
    … …  
    546717                        'item_link_description'      => __( 'A link to a kind', 'indieweb-post-kinds' ),
    547718                );
     719
     720                // The slug is used as the name too: core finds the default term by name, and a translated name would not match.
     721                $default_kind = get_option( 'kind_default', 'note' );
    548722
    549723                $args = array(
    … …  
    560734                        'show_admin_column'  => true,
    561735                        'meta_box_cb'        => array( self::class, 'select_metabox' ),
    562                         'rewrite'            => true,
     736                        'rewrite'            => array( 'slug' => self::get_kind_base() ),
    563737                        'query_var'          => true,
    564738                        'default_term'       => array(
    565                                 'name' => __( 'Article', 'indieweb-post-kinds' ),
    566                                 'slug' => 'article',
     739                                'name' => $default_kind,
     740                                'slug' => $default_kind,
    567741                        ),
    568742
    569743                );
    570744                register_taxonomy( 'kind', array( 'post' ), $args );
    571                 add_permastruct( 'kind_date', 'kind/%kind%/%year%/%monthnum%/' );
    572 
     745                add_permastruct( 'kind_date', self::get_kind_base() . '/%kind%/%year%/%monthnum%/' );
     746
     747                /**
     748                 * Filters the taxonomies that get kind archive rewrite rules.
     749                 *
     750                 * Each taxonomy gets rules for /kind/{kind}/{taxonomy}/{term}/, where kind is the Kind base. Rewrite rules
     751                 * are flushed when the plugin is activated or updated; flush them yourself
     752                 * (Settings > Permalinks > Save) after changing this.
     753                 *
     754                 * @since 3.4.15
     755                 *
     756                 * @param string[] $taxonomies Taxonomy names. Default 'post_tag', 'category' and 'series'.
     757                 */
    573758                $kind_rewrite_taxonomies = apply_filters( 'kind_rewrite_taxonomies', array( 'post_tag', 'category', 'series' ) );
    574759
    575760                // For each kind, support filtering by other taxonomies.
    576761                foreach ( $kind_rewrite_taxonomies as $taxonomy ) {
    577                         add_permastruct( 'kind_' . $taxonomy, 'kind/%kind%/' . $taxonomy . '/%' . $taxonomy . '%' );
    578                 }
    579 
     762                        add_permastruct( 'kind_' . $taxonomy, self::get_kind_base() . '/%kind%/' . $taxonomy . '/%' . $taxonomy . '%' );
     763                }
     764
     765                /**
     766                 * Filters the URL slug for archives that exclude kinds, as in /exclude/kind/{kinds}/.
     767                 *
     768                 * Flush rewrite rules after changing this.
     769                 *
     770                 * @since 3.4.15
     771                 *
     772                 * @param string $slug Slug. Default 'exclude'.
     773                 */
    580774                $kind_exclude_slug = apply_filters( 'kind_exclude_slug', 'exclude' );
    581775
    … …  
    584778                add_permastruct( 'kind_excludes', $kind_exclude_slug . '/%kind_exclude%/%kind_exclude_terms%' );
    585779
     780                /**
     781                 * Filters the URL slug for the archive of posts with photos.
     782                 *
     783                 * Flush rewrite rules after changing this.
     784                 *
     785                 * @since 3.4.15
     786                 *
     787                 * @param string $slug Slug. Default 'photos'.
     788                 */
    586789                $kind_photos_slug   = apply_filters( 'kind_photos_slug', 'photos' );
     790                /**
     791                 * Filters the URL slug for the firehose feed, which includes every kind.
     792                 *
     793                 * Flush rewrite rules after changing this.
     794                 *
     795                 * @since 3.6.0
     796                 *
     797                 * @param string $slug Slug. Default 'firehose'.
     798                 */
    587799                $kind_firehose_slug = apply_filters( 'kind_firehose_slug', 'firehose' );
    588800
    589801                $year_regex       = '([0-9]{4})';
    590                 $month_regex      = '(0-9]{2})';
     802                $month_regex      = '([0-9]{2})';
    591803                $day_regex        = '([0-9]{1,})';
    592804                $pagination_regex = self::get_pagination_regex();
    593805                $feed_regex       = self::get_feed_regex();
    594806                $feed_regex2      = self::get_feed_regex( false );
    595                 $tax_regex        = '([^/]*)';
    596                 $term_regex       = '([^/]*)';
    597 
    598                 // Year Archives for Photos
     807
     808                // Year Archives for Photos.
    599809                add_rewrite_rule(
    600810                        self::generate_permastruct( array( $kind_photos_slug, $year_regex, $pagination_regex ) ),
    … …  
    608818                );
    609819
    610                 // Year and Month Archive for Photos
     820                // Year and Month Archive for Photos.
    611821                add_rewrite_rule(
    612822                        self::generate_permastruct( array( $kind_photos_slug, $year_regex, $month_regex, $pagination_regex ) ),
    … …  
    620830                );
    621831
    622                 // Year and Month And Day Archive for Photos
     832                // Year and Month And Day Archive for Photos.
    623833                add_rewrite_rule(
    624834                        self::generate_permastruct( array( $kind_photos_slug, $year_regex, $month_regex, $day_regex, $pagination_regex ) ),
    … …  
    632842                );
    633843
    634                 // Month And Day Archive for Photos
     844                // Month And Day Archive for Photos.
    635845                add_rewrite_rule(
    636846                        self::generate_permastruct( array( $kind_photos_slug, $month_regex, $day_regex, $pagination_regex ) ),
    … …  
    698908
    699909        /**
     910         * Points the kind taxonomy's default term at the Default Kind setting.
     911         *
     912         * Core sets the default term when the taxonomy is registered; this applies a
     913         * change to the setting within the same request.
     914         *
     915         * @since 4.0.0
     916         */
     917        public static function sync_default_term() {
     918                $term = get_term_by( 'slug', get_option( 'kind_default', 'note' ), 'kind' );
     919                if ( $term ) {
     920                        update_option( 'default_term_kind', $term->term_id );
     921                }
     922        }
     923
     924        /**
    700925         * Sets up Default Terms for Kind Taxonomy.
     926         *
     927         * Creates a term for each registered kind that has none, and updates the
     928         * name and description of terms that differ from the kind's, for example
     929         * after a translation changes. Terms that already match are not written.
     930         *
     931         * @since 2.0.2
    701932         */
    702933        public static function kind_defaultterms() {
    703                 $terms = self::get_kind_list();
    704                 foreach ( $terms as $term ) {
    705                         if ( ! get_term_by( 'slug', $term, 'kind' ) ) {
     934                $existing = get_terms(
     935                        array(
     936                                'taxonomy'   => 'kind',
     937                                'hide_empty' => false,
     938                        )
     939                );
     940                $existing = is_wp_error( $existing ) ? array() : array_column( $existing, null, 'slug' );
     941                foreach ( self::get_kind_list() as $term ) {
     942                        if ( ! isset( $existing[ $term ] ) ) {
    706943                                self::create_post_kind( $term );
    707                         } else {
     944                                continue;
     945                        }
     946                        $kind = self::get_post_kind_info( $term );
     947                        if ( $kind && ( $existing[ $term ]->name !== $kind->singular_name || $existing[ $term ]->description !== $kind->description ) ) {
    708948                                self::update_post_kind( $term );
    709949                        }
    … …  
    714954         * Update our post kind if the term already exists.
    715955         *
     956         * @since 3.1.0
     957         *
    716958         * @access private
    717959         *
    718          * @param $term
     960         * @param string $term Post kind slug.
    719961         */
    720962        private static function update_post_kind( $term ) {
    … …  
    740982         * Create our post kind term.
    741983         *
     984         * @since 3.1.0
     985         *
    742986         * @access private
    743987         *
    744          * @param $term
     988         * @param string $term Post kind slug.
    745989         */
    746990        private static function create_post_kind( $term ) {
    747991                $kind = self::get_post_kind_info( $term );
    748992                if ( $kind ) {
     993                        // The first argument is the term name; wp_insert_term() has no 'name' argument.
    749994                        wp_insert_term(
    750                                 $kind->slug,
     995                                $kind->singular_name,
    751996                                'kind',
    752997                                array(
    753                                         'name'        => $kind->singular_name,
    754998                                        'description' => $kind->description,
    755999                                        'slug'        => $term,
    … …  
    7621006         * Filter our post kind permalink.
    7631007         *
     1008         * @since 2.0.2
     1009         *
    7641010         * @access public
    7651011         *
    … …  
    7731019                        return $permalink; }
    7741020
    775                 // Get post
     1021                // Get post.
    7761022                $post = get_post( $post_id );
    7771023                if ( ! $post ) {
    7781024                        return $permalink; }
    7791025
    780                 // Get taxonomy terms
    781                 $terms = wp_get_object_terms( $post->ID, 'kind' );
    782                 if ( ! is_wp_error( $terms ) && ! empty( $terms ) && is_object( $terms[0] ) ) {
     1026                // Get taxonomy terms; get_the_terms() uses the term cache the main query primes.
     1027                $terms = get_the_terms( $post, 'kind' );
     1028                if ( is_array( $terms ) && ! empty( $terms ) && is_object( $terms[0] ) ) {
    7831029                        $taxonomy_slug = $terms[0]->slug;
    7841030                } else {
    … …  
    7891035        /**
    7901036         * Fetch the current post kind terms from the current query.
     1037         *
     1038         * @since 3.0.3
    7911039         *
    7921040         * @access public
    … …  
    8121060         * Prefix Introduced in WP5.5
    8131061         *
     1062         * @since 3.5.0
     1063         *
    8141064         * @access public
    8151065         *
    … …  
    8311081         * Original Title and Prefix introduced in WP5.5.
    8321082         *
     1083         * @since 2.0.2
     1084         *
    8331085         * @access public
    8341086         *
    … …  
    8391091         */
    8401092        public static function kind_archive_title( $title, $original_title = null, $prefix = null ) {
    841                 $return     = array();
    842                 $wp_version = get_bloginfo( 'version' );
    843 
     1093                $return = array();
     1094
     1095                /**
     1096                 * This filter is documented in wp-includes/general-template.php
     1097                 */
    8441098                $prefix = apply_filters( 'get_the_archive_title_prefix', $prefix );
    8451099
    … …  
    8841138        /**
    8851139         * Filters the post kind archive description.
     1140         *
     1141         * @since 2.5.2
    8861142         *
    8871143         * @access public
    … …  
    9111167         * Sets Post Format for Post Kind.
    9121168         *
    913          * @param int     $post_id Post ID
    914          * @param WP_Post $post Post Object
    915          * @param boolean $update,
     1169         * @since 2.5.0
     1170         *
     1171         * @param int     $post_id Post ID.
     1172         * @param WP_Post $post    Post object.
     1173         * @param bool    $update  Whether this is an existing post being updated.
    9161174         */
    9171175        public static function post_formats( $post_id, $post, $update ) {
    … …  
    9251183         * Fiters the `<title>` tag parts for the post kind.
    9261184         *
     1185         * @since 3.1.3
     1186         *
    9271187         * @access public
    9281188         *
    … …  
    9321192        public static function document_title_parts( $parts ) {
    9331193                if ( is_tax() || is_category() || is_tag() ) {
    934                         $terms = self::get_terms_from_query();
     1194                        $terms  = self::get_terms_from_query();
     1195                        $return = array();
    9351196                        foreach ( $terms as $term ) {
    9361197                                if ( 'kind' === $term->taxonomy ) {
    … …  
    9501211         * Callback for the taxonomy meta box for our post kinds taxonomy.
    9511212         *
     1213         * @since 2.0.2
     1214         *
    9521215         * @access public
    9531216         *
    … …  
    9551218         */
    9561219        public static function select_metabox( $post ) {
    957                 $include = get_option( 'kind_termslist' );
    958                 $include = array_merge( $include, array( 'note', 'reply', 'article' ) );
    959                 // If Simple Location is Enabled, include the check-in type
    960                 // Filter Kinds
     1220                $include = array_merge( Kind_Config::get_termslist(), array( 'note', 'reply', 'article' ) );
     1221                /**
     1222                 * Filters the kinds offered in the editor and listed by the Kind Menu widget.
     1223                 *
     1224                 * By default these are the kinds selected in the settings, plus note, reply and article.
     1225                 *
     1226                 * @since 2.0.2
     1227                 *
     1228                 * @param string[] $include Kind slugs.
     1229                 */
    9611230                $include = array_unique( apply_filters( 'kind_include', $include ) );
    962                 // Note cannot be removed or disabled without hacking the code
     1231                // Note cannot be removed or disabled without hacking the code.
    9631232                if ( ! in_array( 'note', $include, true ) ) {
    9641233                        $include[] = 'note';
    9651234                }
    966                 if ( isset( $_GET['kind'] ) ) {
    967                         $default = get_term_by( 'slug', $_GET['kind'], 'kind' );
     1235                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Preselects the kind on the new post screen; nothing is saved.
     1236                $requested = ( isset( $_GET['kind'] ) && is_string( $_GET['kind'] ) ) ? sanitize_key( wp_unslash( $_GET['kind'] ) ) : '';
     1237                if ( '' !== $requested ) {
     1238                        $default = get_term_by( 'slug', $requested, 'kind' );
    9681239                } elseif ( 'publish' === get_post_status( $post ) ) {
    969                         // On existing published posts without a kind fall back on article which most closely mimics the behavior of an unclassified post
     1240                        // On existing published posts without a kind fall back on article which most closely mimics the behavior of an unclassified post.
    9701241                        $default = get_term_by( 'slug', 'article', 'kind' );
    9711242                } else {
    … …  
    10031274         * Register our post kinds.
    10041275         *
     1276         * @since 3.1.0
     1277         *
    10051278         * @access public
    10061279         *
    10071280         * @param string $slug Post kind slug.
    1008          * @param array  $args Post kind arguments {
    1009                  * @param string $singular_name  Name for one instance of the kind.
    1010                  * @param string $name   General name for the kind plural.
    1011                  * @param string $verb The string for the verb or action (liked this).
    1012                  * @param string $property Microformats 2 property, superseded by properties
    1013                  * @param array $properties Properties is an array outlining the fields for the particular kind. See Kind Fields class for more details
    1014                  * @param string $format Post Format that maps to this.
    1015                  * @param string $description Description of the Kind
    1016                  * @param url $description-url Link to more information
    1017                  * @param string $title Should this kind have an explicit title.
    1018                  * @param boolean $show Show in Settings.
     1281         * @param array  $args {
     1282         *     Post kind arguments. Arguments that are not listed are kept and can
     1283         *     be read as properties of the Post_Kind.
     1284         *
     1285         *     @type string $singular_name   Name for one instance of the kind. Default the slug.
     1286         *     @type string $name            General name for the kind, plural. Default the slug.
     1287         *     @type string $verb            The verb or action, such as 'Liked'. Default the slug.
     1288         *     @type string $property        Microformats 2 property, such as 'like-of'. Default empty.
     1289         *     @type array  $properties      The fields for the kind. See Kind_Fields.
     1290         *     @type string $format          Post format that maps to this kind. Default 'standard'.
     1291         *     @type string $icon            Icon name. Default the slug.
     1292         *     @type string $description     Description of the kind.
     1293         *     @type string $description_url Link to more information.
     1294         *     @type string $shortlink       Shortlink code for the kind.
     1295         *     @type bool   $title           Whether the kind should have an explicit title.
     1296         *     @type bool   $show            Whether to show the kind in Settings. Default false.
    10191297         * }
    1020          * @return bool
     1298         * @return bool True if the kind was registered, false if it already exists.
    10211299         */
    10221300        public static function register_post_kind( $slug, $args ) {
    10231301                $kind = new Post_Kind( $slug, $args );
    1024                 // Do not allow reregistering existing kinds
     1302                // Do not allow reregistering existing kinds.
    10251303                if ( isset( self::$kinds[ $slug ] ) ) {
    10261304                        return false;
    … …  
    10321310        /**
    10331311         * Retrieve info on a provided post kind.
     1312         *
     1313         * @since 3.1.0
    10341314         *
    10351315         * @access public
    … …  
    10451325        }
    10461326
    1047         // Enable a hidden post kind
    1048 
    10491327        /**
    10501328         * Enables a hidden post kind.
     1329         *
     1330         * @since 3.1.0
    10511331         *
    10521332         * @access public
    … …  
    10671347         * Retrieve list of registered post kinds.
    10681348         *
     1349         * @since 3.1.0
     1350         *
    10691351         * @access public
    10701352         * @return array
    … …  
    10771359         * Returns all translated strings.
    10781360         *
     1361         * @since 2.5.2
     1362         *
    10791363         * @param string $kind     Post Kind to return.
    1080          * @param string $property The individual property
     1364         * @param string $property The individual property.
    10811365         * @return string|array Return kind-property. If either is set to all, return all.
    10821366         */
    … …  
    10951379                        return $k;
    10961380                }
    1097                 if ( ! array_key_exists( $property, get_object_vars( $k ) ) ) {
     1381                if ( ! $k->has( $property ) ) {
    10981382                        return false;
    10991383                }
    … …  
    11031387        /**
    11041388         * Add available webmention links.
     1389         *
     1390         * @since 2.3.5
    11051391         *
    11061392         * @access public
    … …  
    11251411        /**
    11261412         * Add available enclosure links.
     1413         *
     1414         * @since 3.0.4
    11271415         *
    11281416         * @access public
    … …  
    11501438         * Generate a dropdown of our post kinds for the post list table view.
    11511439         *
     1440         * @since 2.5.2
     1441         *
    11521442         * @access public
    11531443         * @param string $post_type Current post type being listed.
    … …  
    11571447                if ( 'post' === $post_type ) {
    11581448                        $taxonomy      = 'kind';
    1159                         $selected      = isset( $_GET[ $taxonomy ] ) ? $_GET[ $taxonomy ] : '';
     1449                        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- A list-table filter; nothing is saved.
     1450                        $selected      = ( isset( $_GET[ $taxonomy ] ) && is_string( $_GET[ $taxonomy ] ) ) ? sanitize_key( wp_unslash( $_GET[ $taxonomy ] ) ) : '';
    11601451                        $kind_taxonomy = get_taxonomy( $taxonomy );
    11611452                        wp_dropdown_categories(
    … …  
    11791470         * Set our post kind terms upon publish transition status.
    11801471         *
    1181          * @access public
    1182          * @param int          $post_id Current post ID
     1472         * @since 2.0.2
     1473         *
     1474         * @access public
     1475         *
     1476         * @param int          $post_id Current post ID.
    11831477         * @param WP_Post|null $post    Current post object.
    11841478         */
    … …  
    11871481                        return;
    11881482                }
    1189                 $count = wp_get_post_terms( $post_id, 'kind' );
    1190                 if ( is_countable( $count ) && $count <= 0 ) {
     1483                $terms = wp_get_post_terms( $post_id, 'kind' );
     1484                if ( ! is_wp_error( $terms ) && empty( $terms ) ) {
    11911485                        set_post_kind( $post_id, get_option( 'kind_default' ) );
    11921486                }
    … …  
    11951489        /**
    11961490         * Maybe set our post kind terms upon post status transition.
     1491         *
     1492         * @since 2.0.2
    11971493         *
    11981494         * @access public
    … …  
    12111507         * Filter the classes on a post's markup.
    12121508         *
     1509         * @since 2.0.2
     1510         *
    12131511         * @access public
    12141512         *
    … …  
    12271525         * Returns a pretty, translated version of a post kind slug.
    12281526         *
     1527         * @since 2.1.1
     1528         *
    12291529         * @param string $slug A post format slug.
    12301530         * @return string The translated post format name.
    … …  
    12321532        public static function get_post_kind_string( $slug ) {
    12331533                $string = self::get_kind_info( $slug, 'singular_name' );
    1234                 return $slug ? $slug : '';
     1534                return $string ? $string : '';
    12351535        }
    12361536
    12371537        /**
    12381538         * Returns a link to a post kind index.
     1539         *
     1540         * @since 2.1.1
    12391541         *
    12401542         * @param string $kind The post kind slug.
    … …  
    12521554         * Retrieve a total count statistic for a given post kind.
    12531555         *
     1556         * @since 3.1.3
     1557         *
    12541558         * @access public
    12551559         *
    … …  
    12671571        /**
    12681572         * Returns the post kind slug for the current post.
     1573         *
     1574         * @since 2.1.1
    12691575         *
    12701576         * @access public
    … …  
    12881594         * Returns the post kind name for the current post.
    12891595         *
    1290          * @access public
    1291          *
    1292          * @param WP_Post|int|null $post
    1293          * @return array|bool|string
     1596         * @since 2.1.1
     1597         *
     1598         * @access public
     1599         *
     1600         * @param WP_Post|int|null $post Optional. Post ID or post object. Default the current post.
     1601         * @return string|false The kind's translated name, or false if the post has no kind.
    12941602         */
    12951603        public static function get_post_kind( $post = null ) {
    … …  
    13061614         * Check if a post has any of the given kinds, or any kind.
    13071615         *
     1616         * @since 2.1.1
     1617         *
    13081618         * @uses has_term()
    13091619         *
    … …  
    13251635         * Assign a kind to a post.
    13261636         *
     1637         * @since 2.1.1
     1638         *
    13271639         * @param int|WP_Post $post The post for which to assign a kind.
    1328          * @param string     $kind A kind to assign. Using an empty string or array will default to article.
    1329          * @return mixed WP_Error on error. Array of affected term IDs on success.
     1640         * @param string      $kind Optional. A registered kind's slug. Default 'article'.
     1641         * @return array|false|WP_Error Array of affected term IDs on success. WP_Error if the post
     1642         *                              or the kind is not valid, or false on failure.
    13301643         */
    13311644        public static function set_post_kind( $post, $kind = 'article' ) {
    … …  
    13411654
    13421655        /**
    1343          * Update callback for REST API endpoint.
    1344          *
    1345          * @access public
    1346          *
    1347          * @param string $kind Post kind being processed.
    1348          * @param $post_array
    1349          */
    1350         public static function set_rest_post_kind( $kind, $post_array ) {
    1351                 if ( isset( $post_array['id'] ) ) {
    1352                         self::set_post_kind( $post_array['id'], $kind );
    1353                 }
     1656         * Update callback for the kind REST field.
     1657         *
     1658         * @since 3.1.0
     1659         *
     1660         * @access public
     1661         *
     1662         * @param string        $kind Post kind to set.
     1663         * @param WP_Post|array $post Post being updated.
     1664         * @return true|WP_Error True on success, WP_Error if the kind could not be set.
     1665         */
     1666        public static function set_rest_post_kind( $kind, $post ) {
     1667                // WordPress passes the post being updated as a WP_Post object.
     1668                $post_id = $post instanceof WP_Post ? $post->ID : ( is_array( $post ) && isset( $post['id'] ) ? (int) $post['id'] : 0 );
     1669                if ( ! $post_id ) {
     1670                        return new WP_Error( 'rest_invalid_post', __( 'Invalid post.', 'indieweb-post-kinds' ), array( 'status' => 400 ) );
     1671                }
     1672                if ( ! current_user_can( 'edit_post', $post_id ) ) {
     1673                        return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to set the kind of this post.', 'indieweb-post-kinds' ), array( 'status' => rest_authorization_required_code() ) );
     1674                }
     1675                $result = self::set_post_kind( $post_id, $kind );
     1676                if ( is_wp_error( $result ) ) {
     1677                        $result->add_data( array( 'status' => 400 ) );
     1678                        return $result;
     1679                }
     1680                return true;
     1681        }
     1682
     1683        /**
     1684         * Get callback for the kind REST field.
     1685         *
     1686         * @since 4.0.0
     1687         *
     1688         * @access public
     1689         *
     1690         * @param array $post Prepared post data, as an array with an id key.
     1691         * @return string|false The post's kind slug, or false if it has none.
     1692         */
     1693        public static function get_rest_post_kind( $post ) {
     1694                // The prepared post is an array; passing it to get_post() would cast it to 1.
     1695                $post_id = is_array( $post ) && isset( $post['id'] ) ? (int) $post['id'] : 0;
     1696                return $post_id ? self::get_post_kind_slug( $post_id ) : false;
    13541697        }
    13551698
    … …  
    13571700         * Whether or not to display the before kind content.
    13581701         *
     1702         * @since 2.6.6
     1703         *
    13591704         * @access public
    13601705         * @return mixed|void
    13611706         */
    13621707        public static function before_kind() {
     1708                /**
     1709                 * Filters whether to show the kind's icon or name before the response.
     1710                 *
     1711                 * Return false to hide it everywhere and remove the display option from the settings page.
     1712                 *
     1713                 * @since 2.6.6
     1714                 *
     1715                 * @param bool $display Whether to show it. Default true.
     1716                 */
    13631717                return apply_filters( 'kind_icon_display', true );
    13641718        }
    … …  
    13671721         * Display before Kind - either icon text or no display.
    13681722         *
     1723         * @since 2.6.6
     1724         *
    13691725         * @param string $kind    The slug for the kind of the current post.
    1370          * @param string $display Override display;
     1726         * @param string $display Override display.
    13711727         * @return string Marked up kind information.
    13721728         */
    … …  
    13781734                        $display = get_option( 'kind_display' );
    13791735                }
    1380                 $text = '<span class="kind-display-text">' . self::get_kind_info( $kind, 'verb' ) . '</span> ';
     1736                $text = '<span class="kind-display-text">' . esc_html( self::get_kind_info( $kind, 'verb' ) ) . '</span> ';
    13811737                $icon = self::get_icon( $kind );
    1382                 // Hide Icon in Feed View
     1738                // Hide Icon in Feed View.
    13831739                if ( 'text' !== $display && is_feed() ) {
    13841740                        $icon = '';
    … …  
    13991755         * Retrieve the icon SVG for a provided post kind.
    14001756         *
     1757         * @since 2.4.0
     1758         *
    14011759         * @access public
    14021760         *
    … …  
    14061764         */
    14071765        public static function get_icon( $kind, $echo = false ) {
     1766                // Icons are shown per post, per widget item and per settings row; read each file once.
     1767                static $svgs = array();
     1768                $kind        = sanitize_key( $kind );
     1769                if ( ! isset( $svgs[ $kind ] ) ) {
     1770                        $svg           = sprintf( '%1$ssvgs/%2$s.svg', plugin_dir_path( __DIR__ ), $kind );
     1771                        $svgs[ $kind ] = file_exists( $svg ) ? (string) file_get_contents( $svg ) : '';
     1772                        // A default size for places without the plugin's stylesheet, such as feed readers. CSS overrides it.
     1773                        $svgs[ $kind ] = preg_replace( '/^<svg\b/', '<svg width="1em" height="1em"', $svgs[ $kind ] );
     1774                }
     1775                if ( '' === $svgs[ $kind ] ) {
     1776                        return '';
     1777                }
    14081778                $name       = self::get_kind_info( $kind, 'singular_name' );
    1409                 $svg        = sprintf( '%1$ssvgs/%2$s.svg', plugin_dir_path( __DIR__ ), $kind );
    1410                 $attributes = apply_filters( 'post_kinds_icon_attributes', 'style="display: inline-block; max-height: 1rem; margin-right: 0.5rem"' );
    1411                 if ( file_exists( $svg ) ) {
    1412                         $return = sprintf( '<span class="svg-icon svg-%1$s" aria-label="%2$s" title="%2$s" %3$s><span aria-hidden="true">%4$s</span></span>', esc_attr( $kind ), esc_attr( $name ), $attributes, file_get_contents( $svg ) );
    1413                 } else {
    1414                         return '';
    1415                 }
     1779                /**
     1780                 * Filters the HTML attributes added to the span that wraps a kind icon.
     1781                 *
     1782                 * The string is output as-is, so it must be safe, escaped markup.
     1783                 *
     1784                 * @since 3.5.8
     1785                 * @since 4.0.0 The default is empty; the stylesheet sizes the icon.
     1786                 *
     1787                 * @param string $attributes HTML attributes. Default empty.
     1788                 */
     1789                $attributes = (string) apply_filters( 'post_kinds_icon_attributes', '' );
     1790                $attributes = '' === $attributes ? '' : ' ' . $attributes;
     1791                $return     = sprintf( '<span class="svg-icon svg-%1$s" aria-label="%2$s" title="%2$s"%3$s><span aria-hidden="true">%4$s</span></span>', esc_attr( $kind ), esc_attr( $name ), $attributes, $svgs[ $kind ] );
    14161792                if ( $echo ) {
    1417                         echo $return; // phpcs:ignore
     1793                        echo $return; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon; the kind and name are escaped, and the attributes come from the post_kinds_icon_attributes filter.
    14181794                }
    14191795                return $return;
  • indieweb-post-kinds/tags/4.0.0/includes/class-kind-view.php

    r3001401 r3738687  
    11<?php
    22/**
    3  * Post Kind View Class
     3 * Kind_View class.
    44 *
    5  * Includes Helper Functions to Set Up Display Behavior and Allows Calling of View Templates
     5 * @package Post_Kinds
     6 * @since   2.0.2
    67 */
    78
    8 // The Kind_View class sets up the kind display behavior for kinds
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Sets up how kinds are displayed, and locates and renders the view templates.
     13 *
     14 * @since 2.0.2
     15 */
    916class Kind_View {
     17        /**
     18         * Adds the hooks that display kind markup.
     19         *
     20         * @since 2.0.2
     21         */
    1022        public static function init() {
    1123
     24                /**
     25                 * Filters whether to add response markup to post content, excerpts and feeds automatically.
     26                 *
     27                 * Return false to place it in the theme instead, for example with
     28                 * Kind_View::get_display().
     29                 *
     30                 * @since 3.6.0
     31                 *
     32                 * @param bool $display Whether to add it. Default true.
     33                 */
    1234                if ( apply_filters( 'kind_content_display', true ) ) {
    1335                        add_filter( 'the_content', array( static::class, 'content_response' ), 9 );
    … …  
    2244        /**
    2345         * Filters the attachment image attributes for image post kinds.
     46         *
     47         * @since 2.6.1
    2448         *
    2549         * @access public
    … …  
    5579        }
    5680
    57         /*
    58          * Function will locate the correct template and return it.
    59          *
    60          * @param string $slug Post Kind Slug.
    61          * @param string $name Post Kind Term Name.
    62          * @return string|null File if located or null if unable to find.
     81        /**
     82         * Finds the view file for a kind, in the theme or the plugin.
     83         *
     84         * @since 3.4.0
     85         *
     86         * @param string $slug View slug, such as 'kind'.
     87         * @param string $name Kind slug, such as 'like'.
     88         * @return string|null Path of the view file, an empty string if no kind is given,
     89         *                     or null if no view file exists.
    6390         */
    6491        public static function locate_view( $slug, $name ) {
    … …  
    6794                        return '';
    6895                }
    69                 $templates[] = "{$slug}-{$name}.php";
    70                 $templates[] = "{$slug}.php";
     96                $templates   = array( "{$slug}-{$name}.php", "{$slug}.php" );
     97                /**
     98                 * Filters the folders searched for view files before the plugin's own views folder.
     99                 *
     100                 * @since 3.3.0
     101                 *
     102                 * @param string[] $paths Folder paths, each with a trailing slash. Default the theme's kind_views folder.
     103                 */
    71104                $look        = apply_filters( 'kind_view_paths', array( get_theme_file_path( 'kind_views/' ) ) );
    72105                $look[]      = plugin_dir_path( __DIR__ ) . 'views/';
    … …  
    94127         * Function will return the output.
    95128         *
     129         * @since 2.4.0
     130         *
    96131         * @access public
    97132         *
    98133         * @param string $slug Post kind slug.
    99134         * @param string $name Post kind term name.
    100          * @param array $args Optional Arguments
     135         * @param array  $args Optional. Arguments for the view. 'post_id' is the post's ID.
    101136         * @return string
    102137         */
    … …  
    124159                }
    125160
    126                 $author = self::get_hcard( $cite['author'] );
     161                // Every author's h-card, as a list such as "Ann, Bob and Cat".
     162                $author = array_filter( array_map( array( static::class, 'get_hcard' ), $cite['authors'] ) );
     163                $author = empty( $author ) ? false : wp_sprintf( '%l', $author );
    127164
    128165                $url   = $cite['url'];
    … …  
    140177         * Return the post kind display.
    141178         *
     179         * @since 2.1.1
     180         *
    142181         * @access public
    143182         *
    … …  
    151190                if ( 'post' === get_post_type( $post_id ) ) {
    152191                        $kind    = get_post_kind_slug( $post_id );
    153                         $content = self::get_view_part( 'kind', $kind );
     192                        $content = self::get_view_part( 'kind', $kind, array( 'post_id' => $post_id ) );
     193                        /**
     194                         * Filters the response markup for a post.
     195                         *
     196                         * @since 2.0.2
     197                         *
     198                         * @param string $content Rendered view markup.
     199                         * @param int    $post_id Post ID.
     200                         */
    154201                        return apply_filters( 'kind_response_display', $content, $post_id );
    155202                }
    … …  
    159206         * Echo the output of get_display.
    160207         *
     208         * @since 2.1.1
     209         *
    161210         * @access public
    162211         * @param int|null $post_id Post ID.
    163212         */
    164213        public static function display( $post_id = null ) {
    165                 echo self::get_display( $post_id ); // phpcs:ignore
     214                echo self::get_display( $post_id ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The view escapes its output as it is built.
    166215        }
    167216
    168217        /**
    169218         * Output the post kind content to the post content.
     219         *
     220         * @since 2.0.2
    170221         *
    171222         * @access public
    … …  
    183234                }
    184235                global $wp_current_filter;
    185                 // Don't allow to be added to the_content more than once (prevent infinite loops)
     236                // Don't allow to be added to the_content more than once (prevent infinite loops).
    186237                $done = false;
    187238                foreach ( $wp_current_filter as $filter ) {
    … …  
    203254         * Output the post kind content to the feed item content.
    204255         *
     256         * @since 3.0.5
     257         *
    205258         * @access public
    206259         *
    … …  
    219272         * Output the post kind content to the content if the `jsonfeed` plugin is active.
    220273         *
     274         * @since 3.0.5
     275         *
    221276         * @access public
    222277         *
    … …  
    228283
    229284                $kind_post = new Kind_Post( $post );
    230                 $kind      = $kind_post->get_kind();
    231                 $type      = Kind_Taxonomy::get_kind_info( $kind, 'property' );
    232                 $cite      = mf2_to_jf2( $kind_post->get_cite() );
     285                $cite      = \ParseThis\mf2_to_jf2( $kind_post->get_cite() );
    233286
    234287                if ( is_string( $cite ) ) {
    … …  
    248301         * Append the post kind display to excerpts.
    249302         *
     303         * @since 2.3.3
     304         *
    250305         * @access public
    251306         *
    … …  
    270325         * This function will remove the www.prefix if it is part of the URL.
    271326         *
     327         * @since 2.3.7
     328         *
    272329         * @access public
    273330         *
    … …  
    283340         * Converts an array of attributes and output them as a string.
    284341         *
     342         * @since 2.1.1
     343         *
    285344         * @access public
    286345         *
    … …  
    301360        /**
    302361         * Converts a URL into a complete `<a>` link with link text.
     362         *
     363         * @since 2.1.1
    303364         *
    304365         * @access public
    … …  
    316377                                $atr = self::get_attributes( $atr );
    317378                }
    318                 $return = '<a ' . $atr . ' href="' . $url . '">' . $name . '</a>';
     379                $return = '<a ' . $atr . ' href="' . esc_url( $url ) . '">' . esc_html( $name ) . '</a>';
    319380                return $return;
    320381        }
    … …  
    323384         * Create formatted HTML output for a field.
    324385         *
    325          * @access public
    326          *
    327          * @param string $field Content to put in the markup
     386         * @since 2.2.0
     387         *
     388         * @access public
     389         *
     390         * @param string $field Content to put in the markup.
    328391         * @param string $attr  Attributes to add to the tag markup.
    329392         * @param string $type  HTML tag type to create. Default span.
    … …  
    333396        public static function get_formatted( $field, $attr, $type = 'span' ) {
    334397                if ( ! isset( $field ) ) {
    335                         return $string;
    336                 }
     398                        return '';
     399                }
     400                $type   = tag_escape( $type );
    337401                $string = '<' . $type . $attr . '>' . $field . '</' . $type . '>';
    338402                return $string;
    … …  
    341405        /**
    342406         * Return post kind-wrapped oEmbed content for a provided URL.
     407         *
     408         * @since 2.1.1
    343409         *
    344410         * @access public
    … …  
    363429                        'dailymotion.com',
    364430                        'flickr.com',
    365                         'imgur.com',
     431                        'imgur.com', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- A domain whose links are embedded with oEmbed, not a remote asset.
    366432                        'issuu.com',
    367433                        'kickstarter.com',
    … …  
    385451                        'youtube.com',
    386452                );
     453                /**
     454                 * Filters the domains whose cited URLs are embedded with oEmbed.
     455                 *
     456                 * @since 3.3.6
     457                 *
     458                 * @param string[] $approvelist Domain names.
     459                 */
    387460                $approvelist = apply_filters( 'post_kind_embed_approvelist', $approvelist );
    388461                if ( ! in_array( $host, $approvelist, true ) ) {
    389462                        return '';
    390463                }
    391                 if ( isset( $GLOBALS['wp_embed'] ) ) {
    392                         $embed = $GLOBALS['wp_embed']->autoembed( $url );
    393                 }
     464                if ( ! isset( $GLOBALS['wp_embed'] ) ) {
     465                        return '';
     466                }
     467                $embed = $GLOBALS['wp_embed']->autoembed( $url );
    394468                if ( 0 === strcmp( $embed, $url ) ) {
    395469                        $embed = '';
    396470                } else {
    397                         $embed = sprintf( '<div class="kind-embed">%1$s<a class="u-url" href="%2$s"></a></div>', $embed, $url );
     471                        // The embed is the oEmbed markup WordPress generated.
     472                        $embed = sprintf( '<div class="kind-embed">%1$s<a class="u-url" href="%2$s"></a></div>', $embed, esc_url( $url ) );
    398473                }
    399474                        return $embed;
    … …  
    402477        /**
    403478         * Returns an array of domains with the post type terminologies
     479         *
     480         * @since 2.1.1
    404481         *
    405482         * @access public
    … …  
    423500                $domain  = self::extract_domain_name( $url );
    424501                if ( array_key_exists( $domain, $strings ) ) {
     502                        /**
     503                         * Filters the phrase for what a known site's URL points to, such as 'a video' for YouTube.
     504                         *
     505                         * It runs only for the sites listed above; other URLs get "a post".
     506                         *
     507                         * @since 2.0.2
     508                         *
     509                         * @param string $string Translated phrase.
     510                         */
    425511                        return apply_filters( 'kind_post_type_string', $strings[ $domain ] );
    426512                } else {
    … …  
    431517        /**
    432518         * Retrieve/Generate the h-card.
     519         *
     520         * @since 2.1.1
    433521         *
    434522         * @param mixed $author The author to generate Accepts an array or optionally other info.
    … …  
    451539
    452540                /**
    453                  * Filter for alternate retrieval types
    454                  *
    455                  * This could be using WordPress's gravatar system, retrieval by pure URL, etc.
    456                  *
    457                  * @param string|boolean $author Defaults to false, but may return string.
    458                  * @param mixed  $author Data on the author, type optional. Defaults to array.
    459                  * @param array  $args        Arguments passed to get_hcard.
     541                 * Filters the author data before it is turned into an h-card.
     542                 *
     543                 * Use it to look up author details another way, for example from a URL
     544                 * or Gravatar.
     545                 *
     546                 * @since 2.3.0
     547                 *
     548                 * @param mixed $author Author data, usually an array with name, url and photo.
     549                 * @param array $args   Arguments passed to Kind_View::get_hcard().
    460550                 */
    461551                $author = apply_filters( 'get_hcard_data', $author, $args );
    … …  
    472562
    473563                /**
    474                  * Filter for alternate presentation
    475                  *
    476                  * @param string|boolean $card Defaults to false, but may return string.
    477                  * @param mixed  $author Data on the author, type optional. Defaults to array.
    478                  * @param array  $args        Arguments passed to get_hcard.
     564                 * Filters the h-card markup for an author.
     565                 *
     566                 * Return non-empty markup to replace the plugin's own h-card. The markup is
     567                 * output as-is, so it must be escaped.
     568                 *
     569                 * @since 2.3.0
     570                 *
     571                 * @param string $card   H-card markup. Default empty, which uses the plugin's markup.
     572                 * @param array  $author Author data, with name, url and photo.
     573                 * @param array  $args   Arguments passed to Kind_View::get_hcard().
    479574                 */
    480575                $card = apply_filters( 'get_hcard', '', $author, $args );
    … …  
    482577                        return $card;
    483578                }
    484                 // Temporarily drop multi-data on display
    485                 if ( ! empty( $author['url'] ) && is_array( $author['url'] ) ) {
    486                         $author['url'] = $author['url'][0];
    487                 }
    488 
    489                 if ( is_array( $author['name'] ) ) {
    490                                 $author['name'] = $author['name'][0];
    491                 }
    492 
    493                 if ( empty( $author['name'] ) && ! empty( $author['url'] ) ) {
     579                // One card is one author: use the first of any list. Kind_Post::normalize_authors() splits lists into cards.
     580                foreach ( array( 'name', 'url', 'photo' ) as $key ) {
     581                        $value          = isset( $author[ $key ] ) ? $author[ $key ] : '';
     582                        $author[ $key ] = is_array( $value ) ? (string) reset( $value ) : (string) $value;
     583                }
     584
     585                // Escape everything here: author data often comes from other sites.
     586                $author['url']   = esc_url( $author['url'] );
     587                $author['photo'] = esc_url( $author['photo'] );
     588                if ( '' === trim( wp_strip_all_tags( $author['name'] ) ) && ! empty( $author['url'] ) ) {
    494589                        $author['name'] = __( 'an author', 'indieweb-post-kinds' );
    495590                }
     591                $author['name']  = esc_html( wp_strip_all_tags( $author['name'] ) );
     592                $args['width']   = absint( $args['width'] );
     593                $args['height']  = absint( $args['height'] );
    496594
    497595                // If no filter generated the card, generate the card.
    … …  
    502600                                }
    503601                                if ( empty( $author['url'] ) ) {
    504                                         return sprintf( '<img src="%1s" class="h-card u-photo p-author" alt="%2s" width=%3s height=%4s />', $author['photo'], $author['name'], $args['width'], $args['height'] );
     602                                        return sprintf( '<img src="%1$s" class="h-card u-photo p-author" alt="%2$s" width="%3$d" height="%4$d" />', $author['photo'], $author['name'], $args['width'], $args['height'] );
    505603                                } else {
    506                                         return sprintf( '<a class="h-card p-author" href="%1s"><img class="u-photo" src="%2s" alt="%3s" width=%4s height=%5s /></a>', $author['url'], $author['photo'], $author['name'], $args['width'], $args['height'] );
     604                                        return sprintf( '<a class="h-card p-author" href="%1$s"><img class="u-photo" src="%2$s" alt="%3$s" width="%4$d" height="%5$d" /></a>', $author['url'], $author['photo'], $author['name'], $args['width'], $args['height'] );
    507605                                }
    508606                                break;
    509607                        case 'name':
    510                                 return sprintf( '<span class="h-card p-author">%1s</span>', $author['name'] );
     608                                return sprintf( '<span class="h-card p-author">%1$s</span>', $author['name'] );
    511609                        case 'both':
    512610                                if ( ! empty( $author['photo'] ) ) {
    513611                                        if ( empty( $author['url'] ) ) {
    514                                                 return sprintf( '<span class="h-card p-author"><img src="%1s" class="u-photo" alt="%2s" width=%3s height=%4s />%5s</span>', $author['photo'], $author['name'], $args['width'], $args['height'], $author['name'] );
     612                                                return sprintf( '<span class="h-card p-author"><img src="%1$s" class="u-photo" alt="%2$s" width="%3$d" height="%4$d" />%5$s</span>', $author['photo'], $author['name'], $args['width'], $args['height'], $author['name'] );
    515613                                        } else {
    516                                                 return sprintf( '<a href="%1s" class="h-card p-author"><img class="u-photo" src="%2s" alt="%3s" width=%4s height=%5s />%6s</a>', $author['url'], $author['photo'], $author['name'], $args['width'], $args['height'], $author['name'] );
     614                                                return sprintf( '<a href="%1$s" class="h-card p-author"><img class="u-photo" src="%2$s" alt="%3$s" width="%4$d" height="%5$d" />%6$s</a>', $author['url'], $author['photo'], $author['name'], $args['width'], $args['height'], $author['name'] );
    517615                                        }
     616                                } elseif ( ! empty( $author['url'] ) ) {
     617                                        return sprintf( '<a href="%1$s" class="h-card p-author">%2$s</a>', $author['url'], $author['name'] );
    518618                                } else {
    519                                         return sprintf( '<span class="h-card p-author">%1s</span>', $author['name'] );
     619                                        return sprintf( '<span class="h-card p-author">%1$s</span>', $author['name'] );
    520620                                }
    521621                                break;
    … …  
    529629         * Retrieve a title for a given citation.
    530630         *
     631         * @since 2.3.0
     632         *
    531633         * @access public
    532634         *
    … …  
    538640                        return false;
    539641                }
    540                 if ( empty( $cite['url'] ) ) {
    541                         if ( ! isset( $cite['name'] ) ) {
     642                // FIXME: Temporary Fix for array functionality.
     643                $url  = isset( $cite['url'] ) ? $cite['url'] : '';
     644                $url  = esc_url( is_array( $url ) ? (string) reset( $url ) : (string) $url );
     645                $name = isset( $cite['name'] ) ? $cite['name'] : null;
     646                $name = is_array( $name ) ? (string) reset( $name ) : $name;
     647                if ( empty( $url ) ) {
     648                        if ( null === $name ) {
    542649                                return '';
    543650                        }
    544                         return sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
    545                 }
    546                 // FIXME: Temporary Fix for array functionality
    547                 if ( is_array( $cite['url'] ) ) {
    548                         $cite['url'] = $cite['url'][0];
    549                 }
    550                 if ( ! array_key_exists( 'name', $cite ) ) {
    551                         // $cite['name'] = self::get_post_type_string( $cite['url'] );
    552                         $cite['name'] = $cite['url'];
    553                 }
    554                 return sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $cite['url'], $cite['name'] );
     651                        return sprintf( '<span class="p-name">%1$s</span>', esc_html( wp_strip_all_tags( (string) $name ) ) );
     652                }
     653                if ( null === $name || '' === $name ) {
     654                        $name = $url;
     655                }
     656                return sprintf( '<a href="%1$s" class="p-name u-url">%2$s</a>', $url, esc_html( wp_strip_all_tags( (string) $name ) ) );
    555657        }
    556658
    557659        /**
    558660         * Retrieve site name for given citation.
     661         *
     662         * @since 2.3.0
    559663         *
    560664         * @access public
    … …  
    570674                        return false;
    571675                }
    572                 return sprintf( '<span class="p-publication">%1s</span>', $cite['publication'] );
    573         }
    574 
    575         /**
    576          * Returns a requested Rating item.
    577          *
    578          * The returned value will be a printf-ready translated string.
    579          *
    580          * @access
    581          *
    582          * @param int $rating Rating Value.
    583          * @return mixed|string
     676                $publication = is_array( $cite['publication'] ) ? (string) reset( $cite['publication'] ) : (string) $cite['publication'];
     677                return sprintf( '<span class="p-publication">%1$s</span>', esc_html( wp_strip_all_tags( $publication ) ) );
     678        }
     679
     680        /**
     681         * Returns the citation's summary as escaped plain text.
     682         *
     683         * Summaries are plain text: tags are stripped, and the text is marked up as
     684         * p-summary. Line breaks are kept.
     685         *
     686         * @since 4.0.0
     687         *
     688         * @access public
     689         *
     690         * @param array $cite Normalized citation (see Kind_Post::normalize_cite()).
     691         * @return string The summary markup, or an empty string if there is none.
     692         */
     693        public static function get_summary( $cite ) {
     694                if ( ! is_array( $cite ) || empty( $cite['summary'] ) ) {
     695                        return '';
     696                }
     697                $summary = $cite['summary'];
     698                if ( is_array( $summary ) ) {
     699                        foreach ( array( 'value', 'text', 'html' ) as $key ) {
     700                                if ( isset( $summary[ $key ] ) ) {
     701                                        $summary = $summary[ $key ];
     702                                        break;
     703                                }
     704                        }
     705                        $summary = is_array( $summary ) ? (string) reset( $summary ) : $summary;
     706                }
     707                $summary = trim( wp_strip_all_tags( (string) $summary ) );
     708                if ( '' === $summary ) {
     709                        return '';
     710                }
     711                return sprintf( '<blockquote class="p-summary">%1$s</blockquote>', nl2br( esc_html( $summary ) ) );
     712        }
     713
     714        /**
     715         * Returns a rating as a row of stars.
     716         *
     717         * @since 3.7.0
     718         *
     719         * @access public
     720         *
     721         * @param int|string $rating Rating value. Values above 10 show 10 stars.
     722         * @return string Plain text: one star per point, or an empty string.
    584723         */
    585724        public static function rating_text( $rating ) {
    586                 if ( ! $rating || 0 === $rating ) {
    587                         return '';
    588                 }
    589 
    590                 $ret = '';
    591                 for ( $i = $rating; $i > 0; $i-- ) {
    592                         $ret .= '⭐';
    593                 }
    594 
    595                 return $ret;
    596         }
    597 
    598         /**
    599          * Returns a requested RSVP option item.
    600          *
    601          * The returned value will be a printf-ready translated string.
    602          *
    603          * @access
    604          *
    605          * @param string $type RSVP type to return.
    606          * @return mixed|string
    607          */
    608         public static function rsvp_text( $type ) {
    609                 if ( ! $type ) {
    610                         return '';
    611                 }
     725                $rating = is_numeric( $rating ) ? (int) $rating : 0;
     726                if ( $rating <= 0 ) {
     727                        return '';
     728                }
     729                return str_repeat( '⭐', min( $rating, 10 ) );
     730        }
     731
     732        /**
     733         * Returns the text for an RSVP.
     734         *
     735         * Called with only a type, it returns a printf-ready string whose
     736         * %1$s and %2$s are the event URL and name, as before 4.0.0; the caller
     737         * must escape both. With a URL or name, it returns the escaped sentence
     738         * with the event linked.
     739         *
     740         * @since 2.6.1
     741         *
     742         * @access public
     743         *
     744         * @param string      $type RSVP value: yes, no, maybe, interested or remote.
     745         * @param string|null $url  Optional. Event URL.
     746         * @param string|null $name Optional. Event name. Defaults to the URL.
     747         * @return string The RSVP text, or an empty string for an unknown type.
     748         */
     749        public static function rsvp_text( $type, $url = null, $name = null ) {
    612750                $rsvp = array(
    613                         /* translators: URL for link to event and name of event */
    614                         'yes'        => __( 'Attending <a href="%1$s" class="u-in-reply-to">%2$s</a>', 'indieweb-post-kinds' ),
    615                         /* translators: URL for link to event and name of event */
    616                         'maybe'      => __( 'Might be attending <a href="%1$s" class="u-in-reply-to">%2$s</a>', 'indieweb-post-kinds' ),
    617                         /* translators: URL for link to event and name of event */
    618                         'no'         => __( 'Unable to Attend <a href="%1$s" class="u-in-reply-to">%2$s</a>', 'indieweb-post-kinds' ),
    619                         /* translators: URL for link to event and name of event */
    620                         'interested' => __( 'Interested in Attending <a href="%1$s" class=u-in-reply-to">%2$s</a>', 'indieweb-post-kinds' ),
    621                         /* translators: URL for link to event and name of event */
    622                         'remote'     => __( 'Attending <a href="%1$s" class="u-in-reply-to">%2$s</a> remotely', 'indieweb-post-kinds' ),
     751                        /* translators: %s: Link to the event */
     752                        'yes'        => __( 'Attending %s', 'indieweb-post-kinds' ),
     753                        /* translators: %s: Link to the event */
     754                        'maybe'      => __( 'Might be attending %s', 'indieweb-post-kinds' ),
     755                        /* translators: %s: Link to the event */
     756                        'no'         => __( 'Unable to Attend %s', 'indieweb-post-kinds' ),
     757                        /* translators: %s: Link to the event */
     758                        'interested' => __( 'Interested in Attending %s', 'indieweb-post-kinds' ),
     759                        /* translators: %s: Link to the event */
     760                        'remote'     => __( 'Attending %s remotely', 'indieweb-post-kinds' ),
    623761                );
    624                 return $rsvp[ $type ];
     762                if ( ! is_string( $type ) || ! array_key_exists( $type, $rsvp ) ) {
     763                        return '';
     764                }
     765                if ( null === $url && null === $name ) {
     766                        return sprintf( esc_html( $rsvp[ $type ] ), '<a href="%1$s" class="u-in-reply-to">%2$s</a>' );
     767                }
     768                $url  = esc_url( (string) $url );
     769                $name = ( null === $name || '' === $name ) ? $url : $name;
     770                $name = esc_html( wp_strip_all_tags( (string) $name ) );
     771                if ( '' === $url ) {
     772                        $link = sprintf( '<span class="p-in-reply-to">%1$s</span>', $name );
     773                } else {
     774                        $link = sprintf( '<a href="%1$s" class="u-in-reply-to">%2$s</a>', $url, $name );
     775                }
     776                return sprintf( esc_html( $rsvp[ $type ] ), $link );
    625777        }
    626778
    627779        /**
    628780         * Returns a requested read status option item.
     781         *
     782         * @since 3.0.7
    629783         *
    630784         * @access public
    … …  
    642796                        'finished' => __( 'Finished Reading: ', 'indieweb-post-kinds' ),
    643797                );
    644                 return $read[ $type ];
     798                return ( is_string( $type ) && isset( $read[ $type ] ) ) ? $read[ $type ] : '';
    645799        }
    646800
    … …  
    648802         * Return a string for a requested duration.
    649803         *
    650          * @access public
    651          *
    652          * @param string|Dateinterval $interval Duration to display.
    653          * @return string
    654          * @throws Exception
     804         * @since 2.6.1
     805         * @since 4.0.0 Returns escaped markup.
     806         *
     807         * @access public
     808         *
     809         * @param string|DateInterval $interval Duration to display.
     810         * @return string The duration as an escaped dt-duration time element, or an empty string if it is not a valid duration.
    655811         */
    656812        public static function display_duration( $interval ) {
    657                 if ( ! $interval instanceof DateInterval ) {
    658                         $interval = new DateInterval( $interval );
    659                 }
     813                $interval = kind_safe_interval( $interval );
    660814                if ( ! $interval ) {
    661815                        return '';
    … …  
    695849                }
    696850
    697                 return sprintf( '<time class="dt-duration" datetime="%1$s">%2$s</time>', date_interval_to_iso8601( $interval ), implode( ' ', $duration ) );
     851                return sprintf( '<time class="dt-duration" datetime="%1$s">%2$s</time>', esc_attr( date_interval_to_iso8601( $interval ) ), esc_html( implode( ' ', $duration ) ) );
    698852        }
    699853}  // End Class
  • indieweb-post-kinds/tags/4.0.0/includes/class-mf2-post.php

    r2973430 r3738687  
    11<?php
     2/**
     3 * MF2_Post class.
     4 *
     5 * @package Post_Kinds
     6 * @since   3.1.0
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
    211/**
    312 * MF2 Post Class
    413 *
    5  * @package Post Kinds
    6  * Assists in retrieving/saving microformats 2 properties from a post
     14 * Assists in retrieving/saving microformats 2 properties from a post.
     15 *
     16 * @since 3.1.0
     17 * @deprecated 4.0.0 Use Kind_Post. MF2_Post will be removed in a future major version.
    718 */
    819class MF2_Post implements ArrayAccess {
     20        /**
     21         * Post ID.
     22         *
     23         * @since 3.1.0
     24         * @var int
     25         */
    926        public $uid;
     27
     28        /**
     29         * ID of the post author.
     30         *
     31         * @since 3.1.0
     32         * @var int|string
     33         */
    1034        public $post_author;
     35
     36        /**
     37         * Post type.
     38         *
     39         * @since 3.3.0
     40         * @var string
     41         */
    1142        public $post_type;
     43
     44        /**
     45         * Author, as an h-card.
     46         *
     47         * @since 3.1.0
     48         * @var array|string|false
     49         */
    1250        public $author;
     51
     52        /**
     53         * Publication the post appeared in.
     54         *
     55         * @since 3.1.0
     56         * @var string
     57         */
    1358        public $publication;
     59
     60        /**
     61         * Published date, in DATE_W3C format.
     62         *
     63         * @since 3.1.0
     64         * @var string
     65         */
    1466        public $published;
     67
     68        /**
     69         * Updated date, in DATE_W3C format.
     70         *
     71         * @since 3.1.0
     72         * @var string
     73         */
    1574        public $updated;
     75
     76        /**
     77         * Content, with 'html' and 'value' versions.
     78         *
     79         * @since 3.1.0
     80         * @var array
     81         */
    1682        public $content;
     83
     84        /**
     85         * Summary (excerpt).
     86         *
     87         * @since 3.1.0
     88         * @var string
     89         */
    1790        public $summary;
     91
     92        /**
     93         * ID of the parent post.
     94         *
     95         * @since 3.1.0
     96         * @var int
     97         */
    1898        public $post_parent;
     99
     100        /**
     101         * Post kind slug, or media type for an attachment.
     102         *
     103         * @since 3.1.0
     104         * @var string|false|null
     105         */
    19106        public $kind;
     107
     108        /**
     109         * Permalink, or file URL for an attachment.
     110         *
     111         * @since 3.1.0
     112         * @var string|false
     113         */
    20114        public $url;
     115
     116        /**
     117         * Post title.
     118         *
     119         * @since 3.1.0
     120         * @var string
     121         */
    21122        public $name;
     123
     124        /**
     125         * Category and tag names.
     126         *
     127         * @since 3.1.0
     128         * @var string[]
     129         */
    22130        public $category = array();
     131
     132        /**
     133         * URL of the featured image.
     134         *
     135         * @since 3.1.0
     136         * @var string
     137         */
    23138        public $featured;
     139
     140        /**
     141         * Properties stored in mf2_ prefixed post meta, without the prefix.
     142         *
     143         * @since 3.1.0
     144         * @var array
     145         */
    24146        private $mf2 = array();
    25147
     148        /**
     149         * Reads a post's properties.
     150         *
     151         * @since 3.1.0
     152         * @deprecated 4.0.0 Use Kind_Post.
     153         *
     154         * @param int|WP_Post|string $post Post ID, post object, or the URL of a post or attachment.
     155         */
    26156        public function __construct( $post ) {
     157                _deprecated_function( __METHOD__, '4.0.0', 'Kind_Post' );
    27158                if ( is_numeric( $post ) ) {
    28159                        $this->uid = (int) $post;
    … …  
    75206        }
    76207
     208        /**
     209         * Returns the published date.
     210         *
     211         * For an attachment, this is the date stored in post meta.
     212         *
     213         * @since 3.3.0
     214         *
     215         * @return string|false The date, or false if there is none.
     216         */
    77217        public function get_published() {
    78218                if ( 'attachment' === $this->post_type ) {
    79                         return $this->meta['published'] ?? '';
     219                        return $this->get_single( $this->mf2['published'] ?? '' );
    80220                }
    81221                return get_the_date( DATE_W3C, $this->uid );
    82222        }
    83223
     224        /**
     225         * Returns the updated date.
     226         *
     227         * For an attachment, this is the date stored in post meta.
     228         *
     229         * @since 3.3.0
     230         *
     231         * @return string|false The date, or false if there is none.
     232         */
    84233        public function get_updated() {
    85234                if ( 'attachment' === $this->post_type ) {
    86                         return $this->meta['updated'] ?? '';
     235                        return $this->get_single( $this->mf2['updated'] ?? '' );
    87236                }
    88237                return get_the_modified_date( DATE_W3C, $this->uid );
    89238        }
    90239
     240        /**
     241         * Returns the publication the post appeared in.
     242         *
     243         * For posts other than attachments, this is the site title.
     244         *
     245         * @since 3.3.0
     246         *
     247         * @return string The publication, or an empty string if an attachment has none.
     248         */
    91249        public function get_publication() {
    92250                if ( 'attachment' !== $this->post_type ) {
    93251                        return get_bloginfo( 'title' );
    94252                }
    95                 return $this->meta['publication'] ?? '';
    96         }
    97 
     253                return $this->get_single( $this->mf2['publication'] ?? '' );
     254        }
     255
     256        /**
     257         * Whether a property exists, for ArrayAccess.
     258         *
     259         * @since 3.2.0
     260         *
     261         * @param string $offset Property name.
     262         * @return bool
     263         */
     264        #[\ReturnTypeWillChange]
    98265        public function offsetExists( $offset ) {
    99266                $vars = get_object_vars( $this );
    … …  
    104271        }
    105272
     273        /**
     274         * Returns a property, for ArrayAccess.
     275         *
     276         * @since 3.2.0
     277         *
     278         * @param string $offset Property name.
     279         * @return mixed The value, or null if it does not exist.
     280         */
     281        #[\ReturnTypeWillChange]
    106282        public function offsetGet( $offset ) {
    107283                $vars = get_object_vars( $this );
    … …  
    115291        }
    116292
     293        /**
     294         * Sets a property, for ArrayAccess.
     295         *
     296         * @since 3.2.0
     297         *
     298         * @param string $offset Property name.
     299         * @param mixed  $value  The value.
     300         */
     301        #[\ReturnTypeWillChange]
    117302        public function offsetSet( $offset, $value ) {
    118303                $this->set( $offset, $value );
    119304        }
    120305
     306        /**
     307         * Deletes a property stored in post meta, for ArrayAccess.
     308         *
     309         * @since 3.2.0
     310         *
     311         * @param string $offset Property name.
     312         */
     313        #[\ReturnTypeWillChange]
    121314        public function offsetUnset( $offset ) {
    122315                $this->delete( $offset );
    123316        }
    124317
     318        /**
     319         * Returns the names of a post's categories and tags.
     320         *
     321         * 'Uncategorized' is left out.
     322         *
     323         * @since 3.2.0
     324         *
     325         * @param int $post_id Post ID.
     326         * @return string[] Category and tag names.
     327         */
    125328        public function get_categories( $post_id ) {
    126329                $category = array();
    127                 // Get a list of categories and extract their names
     330                // Get a list of categories and extract their names.
    128331                $post_categories = get_the_terms( $post_id, 'category' );
    129332                if ( ! empty( $post_categories ) && ! is_wp_error( $post_categories ) ) {
    … …  
    131334                }
    132335
    133                 // Get a list of tags and extract their names
     336                // Get a list of tags and extract their names.
    134337                $post_tags = get_the_terms( $post_id, 'post_tag' );
    135338                if ( ! empty( $post_tags ) && ! is_wp_error( $post_tags ) ) {
    … …  
    142345        }
    143346
     347        /**
     348         * Returns the post kind, or the media type of an attachment.
     349         *
     350         * @since 3.1.0
     351         *
     352         * @return string|false|null The kind slug, or null for an attachment that is not image, video or audio.
     353         */
    144354        private function get_post_kind() {
    145355                if ( is_attachment( $this->uid ) ) {
    … …  
    166376        }
    167377
     378        /**
     379         * Returns the post object.
     380         *
     381         * @since 3.1.0
     382         *
     383         * @return WP_Post|null The post, or null if it does not exist.
     384         */
    168385        public function get_post() {
    169386                return get_post( $this->uid );
    … …  
    172389        /**
    173390         * Is prefix in string.
     391         *
     392         * @since 3.1.0
    174393         *
    175394         * @param  string $source The source string.
    … …  
    183402        /**
    184403         * Returns True if Array is Multidimensional.
     404         *
     405         * @since 3.1.0
    185406         *
    186407         * @param array $arr array.
    … …  
    196417        }
    197418
     419        /**
     420         * Sanitizes HTML with the allowed tags from the plugin settings.
     421         *
     422         * @since 3.1.0
     423         *
     424         * @param mixed $value The value. Anything but a string is returned unchanged.
     425         * @return mixed The sanitized value.
     426         */
    198427        public static function sanitize_content( $value ) {
    199428                if ( ! is_string( $value ) ) {
    … …  
    207436        }
    208437
     438        /**
     439         * Sanitizes a URL or text, recursively.
     440         *
     441         * @since 3.1.0
     442         *
     443         * @param string|array $value The value.
     444         * @return string|array The sanitized value.
     445         */
    209446        public function sanitize_text( $value ) {
    210447                if ( is_array( $value ) ) {
    … …  
    222459         * Retrieve author
    223460         *
     461         * @since 3.1.0
     462         *
    224463         * @return boolean|array The result or false if does not exist.
    225464         */
    226465        public function get_author() {
    227466                if ( ! $this->post_author ) {
    228                         return $this->meta['author'] ?? false;
    229                 }
    230                 // Attachments may have been uploaded by a user but may have metadata for original author
     467                        return $this->get_single( $this->mf2['author'] ?? false );
     468                }
     469                // Attachments may have been uploaded by a user but may have metadata for original author.
    231470                if ( 'attachment' === $this->post_type ) {
    232                         return $this->meta['author'] ?? '';
     471                        return $this->get_single( $this->mf2['author'] ?? '' );
    233472                }
    234473                return array(
    … …  
    243482
    244483        /**
    245          * Sets an array with only the mf2 prefixed meta.
    246          *
     484         * Returns the properties stored in mf2_ prefixed post meta, without the prefix.
     485         *
     486         * @since 3.1.0
    247487         */
    248488        private function get_mf2meta() {
    … …  
    251491                        return array();
    252492                }
    253                 if ( isset( $meta['response'] ) ) {
    254                         $response = maybe_unserialize( $meta['response'] );
    255                         // Retrieve from the old response array and store in new location.
    256                         if ( ! empty( $response ) ) {
     493                // Very old versions stored the citation in the response meta. Read it as
     494                // mf2_cite when there is no newer citation; the upgrade routine moves it.
     495                if ( isset( $meta['response'] ) && ! isset( $meta['mf2_cite'] ) ) {
     496                        $response = maybe_unserialize( $meta['response'][0] );
     497                        if ( is_array( $response ) ) {
    257498                                $new = array();
    258                                 // Convert to new format and update.
    259499                                if ( ! empty( $response['title'] ) ) {
    260500                                        $new['name'] = $response['title'];
    … …  
    270510                                }
    271511                                if ( ! empty( $response['author'] ) ) {
    272                                         $new['card']         = array();
    273                                         $new['card']['name'] = $response['author'];
     512                                        $new['card'] = array( 'name' => $response['author'] );
    274513                                        if ( ! empty( $response['icon'] ) ) {
    275514                                                $new['card']['photo'] = $response['icon'];
    276515                                        }
    277516                                }
    278                                 $new         = array_unique( $new );
    279                                 $new['card'] = array_unique( $new['card'] );
    280                                 if ( isset( $new ) ) {
    281                                         update_post_meta( $this->uid, 'mf2_cite', $new );
    282                                         delete_post_meta( $this->uid, 'response' );
    283                                         $meta['cite'] = $new;
     517                                if ( $new ) {
     518                                        $meta['mf2_cite'] = array( $new );
    284519                                }
    285520                        }
    … …  
    291526                                unset( $meta[ $key ] );
    292527                                $key = str_replace( 'mf2_', '', $key );
    293                                 // Do not save microput prefixed instructions
     528                                // Do not save microput prefixed instructions.
    294529                                if ( self::str_prefix( $key, 'mp-' ) ) {
    295530                                        continue;
    … …  
    312547         * Map Properties Based on Post Type
    313548         *
     549         * @since 3.3.0
    314550         */
    315551        private function get_post_type_properties() {
    … …  
    325561         * Retrieve value
    326562         *
    327          * @param  string $key The key to retrieve.
    328          * @param  boolean $single Whether to return a a single value or array if there is only one value.
     563         * @since 3.1.0
     564         *
     565         * @param string|null $key    Optional. The key to retrieve. Default null, for every property.
     566         * @param bool        $single Optional. Whether to unwrap a single value from its array. Default true.
    329567         * @return boolean|string|array The result or false if does not exist.
    330568         */
    … …  
    374612        }
    375613
     614        /**
     615         * Whether the post has a property.
     616         *
     617         * @since 3.1.0
     618         *
     619         * @param string $key Property name.
     620         * @return bool
     621         */
    376622        public function has_key( $key ) {
    377623                $keys = array_merge( get_object_vars( $this ), $this->mf2 );
    … …  
    379625        }
    380626
     627        /**
     628         * Unwraps single-item arrays, recursively.
     629         *
     630         * @since 3.1.0
     631         *
     632         * @param mixed $value   The value to unwrap.
     633         * @param bool  $discard Optional. Whether to keep only the first item of a list
     634         *                       with more than one item. Default false.
     635         * @return mixed The value, with single-item arrays replaced by their item.
     636         */
    381637        private function single_array( $value, $discard = false ) {
    382638                if ( ! is_array( $value ) ) {
    … …  
    395651        }
    396652
     653        /**
     654         * Stores a property, or several.
     655         *
     656         * @since 3.1.0
     657         *
     658         * @param string|array $key   Property name, an array of property => value pairs, or an mf2 item.
     659         * @param mixed        $value Optional. The value. Default null.
     660         * @return mixed The result of storing the value, or false or null if nothing was stored.
     661         */
    397662        public function set( $key, $value = null ) {
    398663                if ( ! $key ) {
    … …  
    542807        }
    543808
     809        /**
     810         * Deletes a property stored in post meta.
     811         *
     812         * @since 3.1.0
     813         *
     814         * @param string $key Property name.
     815         * @return bool True on success, false on failure.
     816         */
    544817        public function delete( $key ) {
    545818                return delete_post_meta( $this->uid, 'mf2_' . $key );
    546819        }
    547820
     821        /**
     822         * Converts mf2 to jf2.
     823         *
     824         * @since 3.1.0
     825         *
     826         * @param array $cite The mf2 item.
     827         * @return array The jf2 item.
     828         */
    548829        public function mf2_to_jf2( $cite ) {
    549830                return mf2_to_jf2( $cite );
    550831        }
    551832
     833        /**
     834         * Returns the first item of an array.
     835         *
     836         * @since 3.1.0
     837         *
     838         * @param mixed $value The value.
     839         * @return mixed The first item, or the value if it is not an array.
     840         */
    552841        public function get_single( $value ) {
    553842                if ( is_array( $value ) ) {
    … …  
    557846        }
    558847
     848        /**
     849         * Converts jf2 to mf2.
     850         *
     851         * @since 3.1.0
     852         *
     853         * @param array  $item The jf2 item.
     854         * @param string $type Optional. jf2 type to use if the item has none. Default 'cite'.
     855         * @return array The mf2 item.
     856         */
    559857        public function jf2_to_mf2( $item, $type = 'cite' ) {
    560858                if ( is_array( $item ) && isset( $item['type'] ) && ! isset( $item['properties'] ) ) {
    … …  
    565863        }
    566864
    567         // Retrieve the right property to use for the link preview based on the kind.
    568         // It will return an array of properties or false if it cannot find what it needs.
    569         // Also will update old posts with new settings
     865        /**
     866         * Returns a property for the link preview, as jf2.
     867         *
     868         * Updates a property stored in the format used before 2.7.0.
     869         *
     870         * @since 3.1.0
     871         *
     872         * @param string $property Property name.
     873         * @return array|false The property, or false if there is none.
     874         */
    570875        public function fetch( $property ) {
    571876
    572                 // If the property is not set then exit
     877                // If the property is not set then exit.
    573878                if ( ! $property || ! $this->has_key( $property ) ) {
    574879                        return false;
    … …  
    578883                        $return = array_shift( $return );
    579884                }
    580                 // If it is in fact a string it is the pre 2.7.0 format and should be updated
     885                // If it is in fact a string it is the pre 2.7.0 format and should be updated.
    581886                if ( is_string( $return ) ) {
    582887                        if ( $this->has_key( 'cite' ) ) {
    … …  
    596901        }
    597902
     903        /**
     904         * Returns the IDs of media attached to a post.
     905         *
     906         * @since 3.1.0
     907         *
     908         * @param string      $type MIME type, such as 'image'.
     909         * @param int|WP_Post $post Post ID or post object.
     910         * @return int[] Attachment IDs.
     911         */
    598912        public function get_attached_media( $type, $post ) {
    599913                $posts = get_attached_media( $type, $post );
    … …  
    601915        }
    602916
     917        /**
     918         * Returns the post's audio.
     919         *
     920         * @since 3.1.0
     921         *
     922         * @return int[]|false Attachment IDs, or false if there are none.
     923         */
    603924        public function get_audios() {
    604925                // Check if the post itself if an audio attachment.
    … …  
    615936        }
    616937
     938        /**
     939         * Returns the post's videos.
     940         *
     941         * @since 3.1.0
     942         *
     943         * @return int[]|false Attachment IDs, or false if there are none.
     944         */
    617945        public function get_videos() {
    618946                // Check if the post itself if an audio attachment.
    … …  
    629957        }
    630958
     959        /**
     960         * Returns the post's images.
     961         *
     962         * @since 3.1.0
     963         *
     964         * @param bool $content_allow Optional. Whether to return images in the content. Default false.
     965         * @return int[]|false Attachment IDs, or false if there are none.
     966         */
    631967        public function get_images( $content_allow = false ) {
    632968                // Check if the post itself is an image attachment.
    … …  
    636972                $post_content = $this->content['html'] ?? '';
    637973                if ( $post_content ) {
     974                        // Recorded when the post is saved; see Kind_Media_Metadata::save_post().
    638975                        $att_ids = get_post_meta( $this->uid, '_content_img_ids', true );
    639                         if ( false === $att_ids ) {
    640                                 $att_ids = Kind_Media_Metadata::get_img_from_content( $post->post_content );
    641                                 update_post_meta( $this->uid, '_content_img_ids', $att_ids );
    642                         }
    643976                        if ( $att_ids ) {
    644977                                return $content_allow ? $att_ids : array();
    … …  
    648981                        }
    649982                }
    650                 // If there is a featured image return only that. Otherwise return all images
     983                // If there is a featured image return only that. Otherwise return all images.
    651984                $featured = get_post_thumbnail_id( $this->uid );
    652985                if ( $featured ) {
    … …  
    655988                $att_ids = $this->get_attached_media( 'image', $this->uid );
    656989                $photos  = $this->get( 'photo', false );
    657                 if ( is_array( $photos ) ) {
    658                         if ( ! wp_is_numeric_array( $photos ) ) {
    659                                 $photos = array( $photos );
    660                         }
    661 
    662                         $photos = $this->sideload_images( $photos );
    663                         $this->set( 'photo', $photos );
     990                if ( is_array( $photos ) && ! wp_is_numeric_array( $photos ) ) {
     991                        $photos = array( $photos );
    664992                }
    665993                $att_ids = array_merge( $att_ids, $this->get_attachments_from_urls( $photos ) );
    … …  
    670998        }
    671999
    672         private function media_sideload_image( $url, $post_id, $description = null ) {
    673                 // To prevent sideloading the same image multiple times check for the original URL which will now be stored
    674                 $ids = get_posts(
    675                         array(
    676                                 'post_type'        => 'attachment',
    677                                 'suppress_filters' => false,
    678                                 'nopaging'         => true,
    679                                 'meta_key'         => 'mf2_url',
    680                                 'meta_value'       => $url,
    681                                 'fields'           => 'ids',
    682                         )
    683                 );
    684                 if ( ! empty( $ids ) ) {
    685                         return $ids[0];
    686                 }
    687                 $id = media_sideload_image( $url, $post_id, $description, 'id' );
    688                 if ( $id ) {
    689                         update_post_meta( $id, 'mf2_url', $url );
    690                 }
    691                 return $id;
    692         }
    693 
    694 
    695         private function sideload_images( $photos ) {
    696                 require_once ABSPATH . 'wp-admin/includes/media.php';
    697                 require_once ABSPATH . 'wp-admin/includes/file.php';
    698                 require_once ABSPATH . 'wp-admin/includes/image.php';
    699                 foreach ( $photos as $key => $value ) {
    700                         if ( is_string( $value ) ) {
    701                                 if ( ! wp_http_validate_url( $value ) ) {
    702                                         continue;
    703                                 } elseif ( ! attachment_url_to_postid( $value ) ) {
    704                                         $id = self::media_sideload_image( $value, $this->uid );
    705                                         if ( $id ) {
    706                                                 $photos[ $key ] = wp_get_attachment_url( $id );
    707                                         }
    708                                 }
    709                         }
    710                         // Attempt to normalize old data
    711                         if ( is_array( $value ) ) {
    712                                 $value = mf2_to_jf2( $value );
    713                                 $id    = attachment_url_to_postid( $value['url'] );
    714                                 if ( ! $id ) {
    715                                         $id = self::media_sideload_image( $value['url'], $this->uid );
    716                                         if ( $id ) {
    717                                                 $value['url'] = wp_get_attachment_url( $id );
    718                                         }
    719                                 }
    720                                 $args = array(
    721                                         'ID'           => $id,
    722                                         'post_title'   => $value['name'] ?? '',
    723                                         'post_excerpt' => $value['summary'] ?? '',
    724                                 );
    725                                 $args = array_filter( $args );
    726                                 wp_update_post( $args );
    727                                 unset( $value['name'] );
    728                                 unset( $value['summary'] );
    729                                 foreach ( $value as $k => $v ) {
    730                                         update_post_meta( $id, 'mf2_' . $k, $v );
    731                                 }
    732                                 $photos[ $key ] = $value['url'];
    733                         }
    734                 }
    735                 return $photos;
    736         }
    737 
     1000        /**
     1001         * Looks up the attachment IDs for media URLs.
     1002         *
     1003         * @since 3.1.0
     1004         *
     1005         * @param string|array $urls A URL, or a list of URLs or arrays with a 'url' key.
     1006         * @return int[] Attachment IDs found.
     1007         */
    7381008        public function get_attachments_from_urls( $urls ) {
    7391009                if ( is_string( $urls ) ) {
  • indieweb-post-kinds/tags/4.0.0/includes/class-post-kind.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Post Kind Class
     3 * Post_Kind class.
    44 *
    5  * @package Post Kind
    6  * Used to define a Post Kind object
     5 * @package Post_Kinds
     6 * @since   3.1.0
    77 */
    88
    9 final class Post_Kind {
    10         public $id; // Term ID
    11         public $slug; // Kind Slug
    12         public $name; // Name of Kind - Plural
    13         public $singular_name; // Name of Kind - Singular
    14         public $verb; // The string for the verb or action (liked this)
    15         public $format; // Post Format that Maps to This
    16         public $icon; // Icon
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Defines a single post kind, as registered with register_post_kind().
     13 *
     14 * @since 3.1.0
     15 */
     16final class Post_Kind implements JsonSerializable {
     17        /**
     18         * Term ID.
     19         *
     20         * @since 3.1.0
     21         * @var int
     22         */
     23        public $id;
     24
     25        /**
     26         * Kind slug.
     27         *
     28         * @since 3.1.0
     29         * @var string
     30         */
     31        public $slug;
     32
     33        /**
     34         * General name for the kind, plural.
     35         *
     36         * @since 3.1.0
     37         * @var string
     38         */
     39        public $name;
     40
     41        /**
     42         * Name for one instance of the kind.
     43         *
     44         * @since 3.1.0
     45         * @var string
     46         */
     47        public $singular_name;
     48
     49        /**
     50         * The verb or action, such as 'Liked'.
     51         *
     52         * @since 3.1.0
     53         * @var string
     54         */
     55        public $verb;
     56
     57        /**
     58         * Post format that maps to this kind.
     59         *
     60         * @since 3.1.0
     61         * @var string
     62         */
     63        public $format;
     64
     65        /**
     66         * Icon name.
     67         *
     68         * @since 3.1.0
     69         * @var string
     70         */
     71        public $icon;
     72
     73        /**
     74         * Description of the kind.
     75         *
     76         * @since 3.1.0
     77         * @var string
     78         */
    1779        public $description;
     80
     81        /**
     82         * Link to more information about the kind.
     83         *
     84         * @since 3.1.0
     85         * @var string
     86         */
    1887        public $description_url;
    19         public $title; // Should this Kind Have an Explicit Title
    20         public $show; // Show in Settings
    21         public $property; // Primary Property
    22         public $properties; // Array of Properties
    23         public $shortlink; // Shortlink Coding per http://tantek.pbworks.com/w/page/21743973/Whistle#design
    24 
     88
     89        /**
     90         * Whether the kind should have an explicit title.
     91         *
     92         * @since 3.1.0
     93         * @var bool
     94         */
     95        public $title;
     96
     97        /**
     98         * Whether to show the kind in Settings.
     99         *
     100         * @since 3.1.0
     101         * @var bool
     102         */
     103        public $show;
     104
     105        /**
     106         * Primary microformats 2 property, such as 'like-of'.
     107         *
     108         * @since 3.1.0
     109         * @var string
     110         */
     111        public $property;
     112
     113        /**
     114         * The fields for the kind. See Kind_Fields.
     115         *
     116         * @since 3.1.0
     117         * @var array
     118         */
     119        public $properties;
     120
     121        /**
     122         * Shortlink code, per http://tantek.pbworks.com/w/page/21743973/Whistle#design.
     123         *
     124         * @since 3.4.0
     125         * @var string
     126         */
     127        public $shortlink;
     128
     129        /**
     130         * Arguments given at registration that are not declared properties, such as
     131         * those a custom kind adds. Kept here rather than as dynamic properties,
     132         * which are deprecated in PHP 8.2, and still readable as $kind->name.
     133         *
     134         * @since 4.0.0
     135         * @var array
     136         */
     137        private $extra = array();
     138
     139        /**
     140         * Defines a kind.
     141         *
     142         * @since 3.1.0
     143         *
     144         * @param string $slug Kind slug.
     145         * @param array  $args Optional. Kind arguments. See Kind_Taxonomy::register_post_kind().
     146         */
    25147        public function __construct( $slug, $args = array() ) {
    26148                $this->slug = $slug;
    … …  
    28150        }
    29151
     152        /**
     153         * Sets the kind's properties from its arguments, filling in defaults.
     154         *
     155         * Arguments that are not declared properties are kept as extra arguments.
     156         *
     157         * @since 3.1.0
     158         *
     159         * @param array $args Kind arguments. See Kind_Taxonomy::register_post_kind().
     160         */
    30161        public function set_props( $args ) {
    31162                $defaults = array(
    … …  
    35166                        'format'        => 'standard',
    36167                        'icon'          => $this->slug,
    37                         'show'          => 'false',
     168                        'show'          => false,
    38169                        'property'      => '',
    39170                );
    40171                $args     = wp_parse_args( $args, $defaults );
    41172                foreach ( $args as $property_name => $property_value ) {
    42                         $this->$property_name = $property_value;
     173                        if ( 'extra' !== $property_name && property_exists( $this, $property_name ) ) {
     174                                $this->$property_name = $property_value;
     175                        } else {
     176                                $this->extra[ $property_name ] = $property_value;
     177                        }
    43178                }
    44179        }
     180
     181        /**
     182         * Reads an extra argument given at registration.
     183         *
     184         * @since 4.0.0
     185         *
     186         * @param string $name Argument name.
     187         * @return mixed The value, or null if it was not given.
     188         */
     189        public function __get( $name ) {
     190                return isset( $this->extra[ $name ] ) ? $this->extra[ $name ] : null;
     191        }
     192
     193        /**
     194         * Whether an extra argument was given at registration.
     195         *
     196         * @since 4.0.0
     197         *
     198         * @param string $name Argument name.
     199         * @return bool
     200         */
     201        public function __isset( $name ) {
     202                return isset( $this->extra[ $name ] );
     203        }
     204
     205        /**
     206         * Sets an extra argument, without creating a dynamic property.
     207         *
     208         * @since 4.0.0
     209         *
     210         * @param string $name  Argument name.
     211         * @param mixed  $value Value.
     212         */
     213        public function __set( $name, $value ) {
     214                $this->extra[ $name ] = $value;
     215        }
     216
     217        /**
     218         * Whether the kind has a declared property or an extra argument of this name.
     219         *
     220         * @since 4.0.0
     221         *
     222         * @param string $name Property or argument name.
     223         * @return bool
     224         */
     225        public function has( $name ) {
     226                return ( 'extra' !== $name && property_exists( $this, $name ) ) || array_key_exists( $name, $this->extra );
     227        }
     228
     229        /**
     230         * Returns the kind's declared properties and extra arguments, for JSON.
     231         *
     232         * @since 4.0.0
     233         *
     234         * @return array
     235         */
     236        #[\ReturnTypeWillChange]
     237        public function jsonSerialize() {
     238                $vars = get_object_vars( $this );
     239                unset( $vars['extra'] );
     240                return array_merge( $this->extra, $vars );
     241        }
    45242}
  • indieweb-post-kinds/tags/4.0.0/includes/compat.php

    r2973430 r3738687  
    11<?php
    2 
    3 // get_feed_build_date introduced in WordPress 5.2
    4 if ( ! function_exists( 'get_feed_build_date' ) ) {
    5 
    6         function get_feed_build_date( $format ) {
    7                 global $wp_query;
    8 
    9                 if ( empty( $wp_query ) || ! $wp_query->have_posts() ) {
    10                         // Fallback to last time any post was modified or published.
    11                         return get_lastpostmodified( 'GMT' );
    12                 }
    13 
    14                         // Extract the post modified times from the posts.
    15                         $modified_times = wp_list_pluck( $wp_query->posts, 'post_modified_gmt' );
    16 
    17                         // If this is a comment feed, check those objects too.
    18                 if ( $wp_query->is_comment_feed() && $wp_query->comment_count ) {
    19                         // Extract the comment modified times from the comments.
    20                         $comment_times = wp_list_pluck( $wp_query->comments, 'comment_date_gmt' );
    21 
    22                                 // Add the comment times to the post times for comparison.
    23                                 $modified_times = array_merge( $modified_times, $comment_times );
    24                 }
    25 
    26                         // Determine the maximum modified time.
    27                         $max_modified_time = max(
    28                                 array_map(
    29                                         function ( $time ) use ( $format ) {
    30                                                                                 return mysql2date( $format, $time, false );
    31                                         },
    32                                         $modified_times
    33                                 )
    34                         );
    35 
    36                         /**
    37                          * Filters the date the last post or comment in the query was modified.
    38                          *
    39                          * @since 5.2.0
    40                          *
    41                          * @param string $max_modified_time Date the last post or comment was modified in the query.
    42                          * @param string $format            The date format requested in get_feed_build_date.
    43                          */
    44                         return apply_filters( 'get_feed_build_date', $max_modified_time, $format );
     2/**
     3 * Polyfills for functions added to WordPress or PHP after the plugin's minimums
     4 * (WordPress 6.2, PHP 7.4).
     5 *
     6 * Each polyfill is a copy of the core function, guarded by function_exists(),
     7 * and notes the version that introduced it so it can be removed once the
     8 * minimum catches up. ClassicPress may already provide some of them.
     9 *
     10 * @package Post_Kinds
     11 */
     12
     13defined( 'ABSPATH' ) || exit;
     14
     15// wp_get_admin_notice() was introduced in WordPress 6.4.
     16if ( ! function_exists( 'wp_get_admin_notice' ) ) {
     17        /**
     18         * Creates and returns the markup for an admin notice.
     19         *
     20         * @since 6.4.0
     21         *
     22         * @param string $message The message.
     23         * @param array  $args {
     24         *     Optional. An array of arguments for the admin notice. Default empty array.
     25         *
     26         *     @type string   $type               Optional. The type of admin notice.
     27         *                                        For example, 'error', 'success', 'warning', 'info'.
     28         *                                        Default empty string.
     29         *     @type bool     $dismissible        Optional. Whether the admin notice is dismissible. Default false.
     30         *     @type string   $id                 Optional. The value of the admin notice's ID attribute. Default empty string.
     31         *     @type string[] $additional_classes Optional. A string array of class names. Default empty array.
     32         *     @type string[] $attributes         Optional. Additional attributes for the notice div. Default empty array.
     33         *     @type bool     $paragraph_wrap     Optional. Whether to wrap the message in paragraph tags. Default true.
     34         * }
     35         * @return string The markup for an admin notice.
     36         */
     37        function wp_get_admin_notice( $message, $args = array() ) {
     38                $defaults = array(
     39                        'type'               => '',
     40                        'dismissible'        => false,
     41                        'id'                 => '',
     42                        'additional_classes' => array(),
     43                        'attributes'         => array(),
     44                        'paragraph_wrap'     => true,
     45                );
     46
     47                $args = wp_parse_args( $args, $defaults );
     48
     49                /**
     50                 * Filters the arguments for an admin notice.
     51                 *
     52                 * @since 6.4.0
     53                 *
     54                 * @param array  $args    The arguments for the admin notice.
     55                 * @param string $message The message for the admin notice.
     56                 */
     57                $args       = apply_filters( 'wp_admin_notice_args', $args, $message );
     58                $id         = '';
     59                $classes    = 'notice';
     60                $attributes = '';
     61
     62                if ( is_string( $args['id'] ) ) {
     63                        $trimmed_id = trim( $args['id'] );
     64
     65                        if ( '' !== $trimmed_id ) {
     66                                $id = 'id="' . $trimmed_id . '" ';
     67                        }
     68                }
     69
     70                if ( is_string( $args['type'] ) ) {
     71                        $type = trim( $args['type'] );
     72
     73                        if ( str_contains( $type, ' ' ) ) {
     74                                _doing_it_wrong(
     75                                        __FUNCTION__,
     76                                        sprintf(
     77                                                /* translators: %s: The "type" key. */
     78                                                esc_html__( 'The %s key must be a string without spaces.', 'default' ),
     79                                                '<code>type</code>'
     80                                        ),
     81                                        '6.4.0'
     82                                );
     83                        }
     84
     85                        if ( '' !== $type ) {
     86                                $classes .= ' notice-' . $type;
     87                        }
     88                }
     89
     90                if ( true === $args['dismissible'] ) {
     91                        $classes .= ' is-dismissible';
     92                }
     93
     94                if ( is_array( $args['additional_classes'] ) && ! empty( $args['additional_classes'] ) ) {
     95                        $classes .= ' ' . implode( ' ', $args['additional_classes'] );
     96                }
     97
     98                if ( is_array( $args['attributes'] ) && ! empty( $args['attributes'] ) ) {
     99                        $attributes = '';
     100                        foreach ( $args['attributes'] as $attr => $val ) {
     101                                if ( is_bool( $val ) ) {
     102                                        $attributes .= $val ? ' ' . $attr : '';
     103                                } elseif ( is_int( $attr ) ) {
     104                                        $attributes .= ' ' . esc_attr( trim( $val ) );
     105                                } elseif ( $val ) {
     106                                        $attributes .= ' ' . $attr . '="' . esc_attr( trim( $val ) ) . '"';
     107                                }
     108                        }
     109                }
     110
     111                if ( false !== $args['paragraph_wrap'] ) {
     112                        $message = "<p>$message</p>";
     113                }
     114
     115                $markup = sprintf( '<div %1$sclass="%2$s"%3$s>%4$s</div>', $id, $classes, $attributes, $message );
     116
     117                /**
     118                 * Filters the markup for an admin notice.
     119                 *
     120                 * @since 6.4.0
     121                 *
     122                 * @param string $markup  The HTML markup for the admin notice.
     123                 * @param string $message The message for the admin notice.
     124                 * @param array  $args    The arguments for the admin notice.
     125                 */
     126                return apply_filters( 'wp_admin_notice_markup', $markup, $message, $args );
    45127        }
    46128}
    47129
    48 // Backcompat for wp_filter_content_tags introduced in 5.5.
    49 if ( ! function_exists( 'wp_filter_content_tags' ) ) {
    50         function wp_filter_content_tags( $content ) {
    51                 return wp_make_content_images_responsive( $content ); // phpcs:ignore
     130// wp_admin_notice() was introduced in WordPress 6.4.
     131if ( ! function_exists( 'wp_admin_notice' ) ) {
     132        /**
     133         * Outputs an admin notice.
     134         *
     135         * @since 6.4.0
     136         *
     137         * @param string $message The message to output.
     138         * @param array  $args {
     139         *     Optional. An array of arguments for the admin notice. Default empty array.
     140         *
     141         *     @type string   $type               Optional. The type of admin notice.
     142         *                                        For example, 'error', 'success', 'warning', 'info'.
     143         *                                        Default empty string.
     144         *     @type bool     $dismissible        Optional. Whether the admin notice is dismissible. Default false.
     145         *     @type string   $id                 Optional. The value of the admin notice's ID attribute. Default empty string.
     146         *     @type string[] $additional_classes Optional. A string array of class names. Default empty array.
     147         *     @type string[] $attributes         Optional. Additional attributes for the notice div. Default empty array.
     148         *     @type bool     $paragraph_wrap     Optional. Whether to wrap the message in paragraph tags. Default true.
     149         * }
     150         */
     151        function wp_admin_notice( $message, $args = array() ) {
     152                /**
     153                 * Fires before an admin notice is output.
     154                 *
     155                 * @since 6.4.0
     156                 *
     157                 * @param string $message The message for the admin notice.
     158                 * @param array  $args    The arguments for the admin notice.
     159                 */
     160                do_action( 'wp_admin_notice', $message, $args );
     161
     162                echo wp_kses_post( wp_get_admin_notice( $message, $args ) );
    52163        }
    53164}
     165
     166// wp_trigger_error() was introduced in WordPress 6.4 (ClassicPress 2.2.0).
     167if ( ! function_exists( 'wp_trigger_error' ) ) {
     168        /**
     169         * Generates a user-level error/warning/notice/deprecation message.
     170         *
     171         * Generates the message when `WP_DEBUG` is true.
     172         *
     173         * @since 6.4.0
     174         *
     175         * @param string $function_name The function that triggered the error.
     176         * @param string $message       The message explaining the error.
     177         *                              The message can contain allowed HTML 'a' (with href), 'code',
     178         *                              'br', 'em', and 'strong' tags and http or https protocols.
     179         *                              If it contains other HTML tags or protocols, the message should be escaped
     180         *                              before passing to this function to avoid being stripped {@see wp_kses()}.
     181         * @param int    $error_level   Optional. The designated error type for this error.
     182         *                              Only works with E_USER family of constants. Default E_USER_NOTICE.
     183         *
     184         * @throws WP_Exception If the error level is E_USER_ERROR. Exception where WP_Exception does not exist.
     185         */
     186        function wp_trigger_error( $function_name, $message, $error_level = E_USER_NOTICE ) {
     187
     188                // Bail out if WP_DEBUG is not turned on.
     189                if ( ! WP_DEBUG ) {
     190                        return;
     191                }
     192
     193                /**
     194                 * Fires when the given function triggers a user-level error/warning/notice/deprecation message.
     195                 *
     196                 * Can be used for debug backtracking.
     197                 *
     198                 * @since 6.4.0
     199                 *
     200                 * @param string $function_name The function that was called.
     201                 * @param string $message       A message explaining what has been done incorrectly.
     202                 * @param int    $error_level   The designated error type for this error.
     203                 */
     204                do_action( 'wp_trigger_error_run', $function_name, $message, $error_level );
     205
     206                if ( ! empty( $function_name ) ) {
     207                        $message = sprintf( '%s(): %s', $function_name, $message );
     208                }
     209
     210                $message = wp_kses(
     211                        $message,
     212                        array(
     213                                'a'      => array( 'href' => true ),
     214                                'br'     => array(),
     215                                'code'   => array(),
     216                                'em'     => array(),
     217                                'strong' => array(),
     218                        ),
     219                        array( 'http', 'https' )
     220                );
     221
     222                if ( E_USER_ERROR === $error_level ) {
     223                        // WP_Exception was also introduced in WordPress 6.4.
     224                        $exception = class_exists( 'WP_Exception' ) ? 'WP_Exception' : 'Exception';
     225                        throw new $exception( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
     226                }
     227
     228                trigger_error( $message, $error_level ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error, WordPress.Security.EscapeOutput.OutputNotEscaped
     229        }
     230}
  • indieweb-post-kinds/tags/4.0.0/includes/kind-functions.php

    r3014138 r3738687  
    11<?php
    22/**
    3  * Kind Functions
     3 * Global functions for registering, reading and displaying kinds.
    44 *
    5  * Global Scoped Functions for Handling Kinds.
    6  *
    7  * @package Post Kinds
     5 * @package Post_Kinds
     6 * @since   2.0.2
    87 */
    98
    10 /**
    11  *
    12  */
    13 function register_post_kind( $slug, $args ) {
    14         Kind_Taxonomy::register_post_kind( $slug, $args );
    15 }
    16 
    17 function set_post_kind_visibility( $slug, $show = true ) {
    18         Kind_Taxonomy::set_post_kind_visibility( $slug, $show );
    19 }
    20 
    21 /**
    22  * Retrieves an array of post kind slugs.
    23  *
    24  * @return array The array of post kind slugs.
    25  */
    26 function get_post_kind_slugs() {
    27         return Kind_Taxonomy::get_post_kind_slugs();
    28 }
    29 
    30 /**
    31  * Returns a pretty, translated version of a post kind slug
    32  *
    33  * @param string $slug A post format slug.
    34  * @return string The translated post format name.
    35  */
    36 function get_post_kind_string( $slug ) {
    37         return Kind_Taxonomy::get_post_kind_string( $slug );
    38 }
    39 
    40 /**
    41  * Returns a link to a post kind index.
    42  *
    43  * @param string $kind The post kind slug.
    44  * @return string The post kind term link.
    45  */
    46 function get_post_kind_link( $kind ) {
    47         return Kind_Taxonomy::get_post_kind_link( $kind );
    48 }
    49 
    50 /**
    51  * Returns the post kind slug for the current post.
    52  *
    53  * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
    54  * @return string The post kind slug.
    55  */
    56 function get_post_kind_slug( $post = null ) {
    57         return Kind_Taxonomy::get_post_kind_slug( $post );
    58 }
    59 
    60 /**
    61  * Returns the post kind name for the current post.
    62  *
    63  * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
    64  * @return string The post kind name.
    65  */
    66 function get_post_kind( $post = null ) {
    67         return Kind_Taxonomy::get_post_kind( $post );
    68 }
    69 
    70 /**
    71  * Check if a post has any of the given kinds, or any kind.
    72  *
    73  * @uses has_term()
    74  *
    75  * @param string|array $kinds Optional. The kind to check.
    76  * @param object|int   $post Optional. The post to check. If not supplied, defaults to the current post if used in the loop.
    77  * @return bool True if the post has any of the given kinds (or any kind, if no kind specified), false otherwise.
    78  */
    79 function has_post_kind( $kinds = array(), $post = null ) {
    80         return Kind_Taxonomy::has_post_kind( $kinds, $post );
    81 }
    82 
    83 /**
    84  * Assign a kind to a post
    85  *
    86  * @param int|object $post The post for which to assign a kind.
    87  * @param string     $kind A kind to assign. Using an empty string or array will default to note.
    88  * @return mixed WP_Error on error. Array of affected term IDs on success.
    89  */
    90 function set_post_kind( $post, $kind ) {
    91         return Kind_Taxonomy::set_post_kind( $post, $kind );
    92 }
    93 
    94 /**
    95  * Return the Displayed Response for a Specific Kind
    96  *
    97  * @param $slug
    98  * @param $name
    99  * @param $args
    100  * @return string
    101  */
    102 
    103 function get_kind_view_part( $slug, $name = null, $args = null ) {
    104         Kind_View::get_view_part( $slug, $name, $args );
    105 }
    106 
    107 function kind_display( $post_id = null ) {
    108                 echo Kind_View::get_display( $post_id ); // phpcs:ignore
    109 }
    110 
    111 function kind_flatten_array( $a ) {
    112         if ( ! is_array( $a ) ) {
     9defined( 'ABSPATH' ) || exit;
     10
     11if ( ! function_exists( 'register_post_kind' ) ) {
     12        /**
     13         * Registers a post kind.
     14         *
     15         * @since 3.1.0
     16         *
     17         * @param string $slug Post kind slug.
     18         * @param array  $args Post kind arguments. See Kind_Taxonomy::register_post_kind().
     19         */
     20        function register_post_kind( $slug, $args ) {
     21                Kind_Taxonomy::register_post_kind( $slug, $args );
     22        }
     23}
     24
     25if ( ! function_exists( 'set_post_kind_visibility' ) ) {
     26        /**
     27         * Shows or hides a post kind in Settings.
     28         *
     29         * @since 3.1.0
     30         *
     31         * @param string $slug Post kind slug.
     32         * @param bool   $show Optional. Whether to show the kind. Default true.
     33         */
     34        function set_post_kind_visibility( $slug, $show = true ) {
     35                Kind_Taxonomy::set_post_kind_visibility( $slug, $show );
     36        }
     37}
     38
     39if ( ! function_exists( 'get_post_kind_slugs' ) ) {
     40        /**
     41         * Retrieves an array of post kind slugs.
     42         *
     43         * @since 0.30
     44         *
     45         * @return array The array of post kind slugs.
     46         */
     47        function get_post_kind_slugs() {
     48                return Kind_Taxonomy::get_kind_list();
     49        }
     50}
     51
     52if ( ! function_exists( 'get_post_kind_string' ) ) {
     53        /**
     54         * Returns a pretty, translated version of a post kind slug.
     55         *
     56         * @since 0.30
     57         *
     58         * @param string $slug A post kind slug.
     59         * @return string The translated post kind name, or an empty string if the kind is not registered.
     60         */
     61        function get_post_kind_string( $slug ) {
     62                return Kind_Taxonomy::get_post_kind_string( $slug );
     63        }
     64}
     65
     66if ( ! function_exists( 'get_post_kind_link' ) ) {
     67        /**
     68         * Returns a link to a post kind index.
     69         *
     70         * @since 0.30
     71         *
     72         * @param string $kind The post kind slug.
     73         * @return string|WP_Error|false The post kind term link, a WP_Error from get_term_link(),
     74         *                               or false if the kind has no term.
     75         */
     76        function get_post_kind_link( $kind ) {
     77                return Kind_Taxonomy::get_post_kind_link( $kind );
     78        }
     79}
     80
     81if ( ! function_exists( 'get_post_kind_slug' ) ) {
     82        /**
     83         * Returns the post kind slug for the current post.
     84         *
     85         * @since 0.30
     86         *
     87         * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
     88         * @return string|false The post kind slug, or false if the post has no kind.
     89         */
     90        function get_post_kind_slug( $post = null ) {
     91                return Kind_Taxonomy::get_post_kind_slug( $post );
     92        }
     93}
     94
     95if ( ! function_exists( 'get_post_kind' ) ) {
     96        /**
     97         * Returns the post kind name for the current post.
     98         *
     99         * @since 0.30
     100         *
     101         * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
     102         * @return string|false The translated post kind name, or false if the post has no kind.
     103         */
     104        function get_post_kind( $post = null ) {
     105                return Kind_Taxonomy::get_post_kind( $post );
     106        }
     107}
     108
     109if ( ! function_exists( 'has_post_kind' ) ) {
     110        /**
     111         * Check if a post has any of the given kinds, or any kind.
     112         *
     113         * @since 0.30
     114         *
     115         * @uses has_term()
     116         *
     117         * @param string|array $kinds Optional. The kind to check.
     118         * @param WP_Post|int  $post  Optional. The post to check. If not supplied, defaults to the current post if used in the loop.
     119         * @return bool True if the post has any of the given kinds (or any kind, if no kind specified), false otherwise.
     120         */
     121        function has_post_kind( $kinds = array(), $post = null ) {
     122                return Kind_Taxonomy::has_post_kind( $kinds, $post );
     123        }
     124}
     125
     126if ( ! function_exists( 'set_post_kind' ) ) {
     127        /**
     128         * Assign a kind to a post.
     129         *
     130         * @since 0.30
     131         *
     132         * @param int|WP_Post $post The post for which to assign a kind.
     133         * @param string      $kind A registered kind's slug.
     134         * @return array|false|WP_Error Array of affected term IDs on success. WP_Error if the post
     135         *                              or the kind is not valid, or false on failure.
     136         */
     137        function set_post_kind( $post, $kind ) {
     138                return Kind_Taxonomy::set_post_kind( $post, $kind );
     139        }
     140}
     141
     142if ( ! function_exists( 'get_kind_view_part' ) ) {
     143        /**
     144         * Returns the rendered view for a kind.
     145         *
     146         * @since 2.5.0
     147         *
     148         * @param string     $slug View slug, such as 'kind'.
     149         * @param string     $name Optional. Kind slug, such as 'like'. Default null.
     150         * @param array|null $args Optional. Arguments for the view. See Kind_View::get_view_part(). Default null.
     151         * @return string The view's markup.
     152         */
     153        function get_kind_view_part( $slug, $name = null, $args = null ) {
     154                return Kind_View::get_view_part( $slug, $name, $args );
     155        }
     156}
     157
     158if ( ! function_exists( 'kind_display' ) ) {
     159        /**
     160         * Outputs the kind markup for a post.
     161         *
     162         * For themes that place the markup themselves instead of adding it to the content.
     163         *
     164         * @since 2.6.1
     165         *
     166         * @param int|null $post_id Optional. Post ID. Default the current post.
     167         */
     168        function kind_display( $post_id = null ) {
     169                        echo Kind_View::get_display( $post_id ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The view escapes its output as it is built.
     170        }
     171}
     172
     173if ( ! function_exists( 'kind_flatten_array' ) ) {
     174        /**
     175         * Removes empty values from an array and unwraps single-item lists, recursively.
     176         *
     177         * @since 2.7.5
     178         *
     179         * @param mixed $a The value to flatten.
     180         * @return mixed The flattened value. An empty string if nothing is left.
     181         */
     182        function kind_flatten_array( $a ) {
     183                if ( ! is_array( $a ) ) {
     184                        return $a;
     185                }
     186                if ( wp_is_numeric_array( $a ) ) {
     187                        $a = array_values( array_filter( array_map( 'kind_flatten_array', $a ) ) );
     188                } else {
     189                        $a = array_filter( $a );
     190                }
     191                if ( empty( $a ) ) {
     192                        return '';
     193                }
     194                if ( 1 === count( $a ) && isset( $a[0] ) ) {
     195                        return $a[0];
     196                }
    113197                return $a;
    114198        }
    115         if ( wp_is_numeric_array( $a ) ) {
    116                 $array = array_map( 'kind_flatten_array', $a );
    117         }
    118         $array = array_filter( $a );
    119         if ( 1 === count( $a ) ) {
    120                 return $a[0];
    121         }
    122 }
    123 
    124 // Return any sort of src urls in content
    125 function kind_src_url_in_content( $content ) {
    126         if ( ! $content ) {
     199}
     200
     201if ( ! function_exists( 'kind_src_url_in_content' ) ) {
     202        /**
     203         * Returns every src attribute value in some markup.
     204         *
     205         * @since 3.0.0
     206         *
     207         * @param string $content The markup.
     208         * @return string[]|int The src URLs, or 0 if there are none.
     209         */
     210        function kind_src_url_in_content( $content ) {
     211                if ( ! $content ) {
     212                        return 0;
     213                }
     214                if ( preg_match_all( '@src="([^"]+)"@', $content, $output ) ) {
     215                        return array_pop( $output );
     216                }
    127217                return 0;
    128218        }
    129         if ( preg_match_all( '@src="([^"]+)"@', $content, $output ) ) {
    130                 return array_pop( $output );
    131         }
    132         return 0;
    133 }
    134 
    135 /**
    136  * Get a Marketed Up Link to the Post.
    137  *
    138  * @param WP_Post|null Post to Display.
    139  * @param string|array Classes to add to link
    140  * @param string|array Classes to add to the date
    141  * @return string Marked up link to a post.
    142  **/
    143 
    144 function kind_get_the_link( $post = null, $cls = null, $date_cls = null ) {
    145         $post = get_post( $post );
    146         $kind = get_post_kind_slug( $post );
    147 
    148         if ( is_array( $cls ) ) {
    149                 $cls = implode( ' ', $cls );
    150         }
    151 
    152         if ( is_array( $date_cls ) ) {
    153                 $date_cls = implode( ' ', $date_cls );
    154         }
    155 
    156         $time_string = '<time class="%3$s" datetime="%1$s">%2$s</time>';
    157         $time_string = sprintf(
    158                 $time_string,
    159                 esc_attr( get_the_date( DATE_W3C, $post ) ),
    160                 get_the_date( '', $post ),
    161                 $date_cls
    162         );
    163         return sprintf( '<a class="%4$s" href="%2$s">%1$s</a> - %3$s', kind_get_the_title( $post, $kind ), get_the_permalink( $post ), $time_string, esc_attr( $cls ) );
    164 }
    165 
    166 
    167 /**
    168  * Get a Generated Title for a Post as Most Post Kinds do Not Have an Explicit Title.
    169  *
    170  * @param WP_Post|null Post to Display.
    171  * @param array $args Arguments.
    172  * @return string Marked up link to a post.
    173  **/
    174 function kind_get_the_title( $post = null, $args = array() ) {
    175         $defaults = array(
    176                 'photo_size' => array( 32, 32 ),
    177         );
    178 
    179         $args = wp_parse_args( $args, $defaults );
    180 
    181         $post      = get_post( $post );
    182         $kind      = get_post_kind_slug( $post );
    183         $title     = get_the_title( $post );
    184         $before    = Kind_Taxonomy::get_before_kind( $kind );
    185         $content   = '';
    186         $kind_post = new Kind_Post( $post );
    187 
    188         if ( ! empty( $title ) ) {
    189                 return $title;
    190         }
    191 
    192         if ( in_array( $kind, array( 'audio', 'video', 'photo' ), true ) ) {
    193                 switch ( $kind ) {
    194                         case 'photo':
    195                                 $photos = $kind_post->get_photo();
    196                                 $before = wp_get_attachment_image(
    197                                         $photos[0],
    198                                         $args['photo_size'],
    199                                         false,
    200                                         array(
    201                                                 'class' => 'kind-photo-thumbnail',
    202                                         )
    203                                 );
    204                 }
    205         } elseif ( ! in_array( $kind, array( 'note', 'article' ), true ) ) {
    206                 $cite = $kind_post->get_cite( 'name' );
    207                 if ( false === $cite ) {
    208                         $content = Kind_View::get_post_type_string( $kind_post->get_cite( 'url' ) );
     219}
     220
     221if ( ! function_exists( 'kind_get_the_link' ) ) {
     222        /**
     223         * Returns a marked up link to a post, followed by its date.
     224         *
     225         * @since 3.5.9
     226         *
     227         * @param WP_Post|int|null  $post     Optional. Post to display. Default the current post.
     228         * @param string|array|null $cls      Optional. Classes to add to the link. Default null.
     229         * @param string|array|null $date_cls Optional. Classes to add to the date. Default null.
     230         * @return string Marked up link to a post.
     231         */
     232        function kind_get_the_link( $post = null, $cls = null, $date_cls = null ) {
     233                $post = get_post( $post );
     234                $kind = get_post_kind_slug( $post );
     235
     236                if ( is_array( $cls ) ) {
     237                        $cls = implode( ' ', $cls );
     238                }
     239
     240                if ( is_array( $date_cls ) ) {
     241                        $date_cls = implode( ' ', $date_cls );
     242                }
     243
     244                $time_string = '<time class="%3$s" datetime="%1$s">%2$s</time>';
     245                $time_string = sprintf(
     246                        $time_string,
     247                        esc_attr( get_the_date( DATE_W3C, $post ) ),
     248                        esc_html( get_the_date( '', $post ) ),
     249                        esc_attr( $date_cls )
     250                );
     251                return sprintf( '<a class="%4$s" href="%2$s">%1$s</a> - %3$s', kind_get_the_title( $post, $kind ), esc_url( get_the_permalink( $post ) ), $time_string, esc_attr( $cls ) );
     252        }
     253}
     254
     255if ( ! function_exists( 'kind_get_the_title' ) ) {
     256        /**
     257         * Returns a post's title, generating one if the post has none.
     258         *
     259         * Most post kinds do not have an explicit title.
     260         *
     261         * @since 3.5.9
     262         *
     263         * @param WP_Post|int|null $post Optional. Post to display. Default the current post.
     264         * @param array            $args {
     265         *     Optional. Arguments.
     266         *
     267         *     @type int[]|string $photo_size Image size for a photo post's thumbnail. Default array( 32, 32 ).
     268         * }
     269         * @return string The title, or the kind's icon or name and generated text.
     270         */
     271        function kind_get_the_title( $post = null, $args = array() ) {
     272                $defaults = array(
     273                        'photo_size' => array( 32, 32 ),
     274                );
     275
     276                $args = wp_parse_args( $args, $defaults );
     277
     278                $post      = get_post( $post );
     279                $kind      = get_post_kind_slug( $post );
     280                $title     = get_the_title( $post );
     281                $before    = Kind_Taxonomy::get_before_kind( $kind );
     282                $content   = '';
     283                $kind_post = new Kind_Post( $post );
     284
     285                if ( ! empty( $title ) ) {
     286                        return $title;
     287                }
     288
     289                if ( in_array( $kind, array( 'audio', 'video', 'photo' ), true ) ) {
     290                        switch ( $kind ) {
     291                                case 'photo':
     292                                        $photos = $kind_post->get_photo();
     293                                        $before = wp_get_attachment_image(
     294                                                $photos[0],
     295                                                $args['photo_size'],
     296                                                false,
     297                                                array(
     298                                                        'class' => 'kind-photo-thumbnail',
     299                                                )
     300                                        );
     301                        }
     302                } elseif ( ! in_array( $kind, array( 'note', 'article' ), true ) ) {
     303                        $cite = $kind_post->get_cite( 'name' );
     304                        if ( false === $cite ) {
     305                                $content = Kind_View::get_post_type_string( $kind_post->get_cite( 'url' ) );
     306                        } else {
     307                                $content = $cite;
     308                        }
    209309                } else {
    210                         $content = $cite;
    211                 }
    212         } else {
    213                 $content = $post->post_excerpt;
    214                 // If no excerpt use content
    215                 if ( ! $content ) {
    216                         $content = $post->post_content;
    217                 }
    218                 // If no content use date
    219                 if ( $content ) {
    220                         $content = mb_strimwidth( wp_strip_all_tags( $content ), 0, 40, '...' );
    221                 }
    222         }
    223         if ( is_array( $content ) ) {
    224                 $content = wp_json_encode( $content );
    225         }
    226 
    227         $content = apply_filters( 'kind_get_the_title_content', $content, $post );
    228         $before  = apply_filters( 'kind_get_the_title_before', $before, $post );
    229 
    230         return trim( sprintf( '%1$s %2$s', $before, $content ) );
    231 }
     310                        $content = $post->post_excerpt;
     311                        // If no excerpt use content.
     312                        if ( ! $content ) {
     313                                $content = $post->post_content;
     314                        }
     315                        // If no content use date.
     316                        if ( $content ) {
     317                                $content = mb_strimwidth( wp_strip_all_tags( $content ), 0, 40, '...' );
     318                        }
     319                }
     320                if ( is_array( $content ) ) {
     321                        $content = wp_json_encode( $content );
     322                }
     323                // The content is text (a cited name or an excerpt), often from another site.
     324                $content = esc_html( wp_strip_all_tags( (string) $content ) );
     325
     326                /**
     327                 * Filters the text of a generated post title.
     328                 *
     329                 * The text is a cited name, an excerpt or similar, already escaped. A callback
     330                 * that adds markup must escape what it adds.
     331                 *
     332                 * @since 3.5.9
     333                 *
     334                 * @param string  $content Escaped title text.
     335                 * @param WP_Post $post    The post.
     336                 */
     337                $content = apply_filters( 'kind_get_the_title_content', $content, $post );
     338                /**
     339                 * Filters the markup shown before a generated post title.
     340                 *
     341                 * The markup is output as-is, so it must be escaped.
     342                 *
     343                 * @since 3.5.9
     344                 *
     345                 * @param string  $before The kind's icon or name, from Kind_Taxonomy::get_before_kind().
     346                 * @param WP_Post $post   The post.
     347                 */
     348                $before  = apply_filters( 'kind_get_the_title_before', $before, $post );
     349
     350                return trim( sprintf( '%1$s %2$s', $before, $content ) );
     351        }
     352}
  • indieweb-post-kinds/tags/4.0.0/includes/register-kinds.php

    r3001401 r3738687  
    11<?php
    2 /* Registers built-in Post Kinds
     2/**
     3 * Registers the built-in kinds.
     4 *
     5 * The arguments each kind takes are documented in
     6 * Kind_Taxonomy::register_post_kind().
     7 *
     8 * @package Post_Kinds
     9 * @since   3.3.4
    310 */
    411
     12defined( 'ABSPATH' ) || exit;
     13
    514register_post_kind(
    615        'article',
    716        array(
    8                 'singular_name'   => __( 'Article', 'indieweb-post-kinds' ), // Name for one instance of the kind
    9                 'name'            => __( 'Articles', 'indieweb-post-kinds' ), // General name for the kind plural
    10                 'verb'            => ' ', // The string for the verb or action (liked this)
    11                 'property'        => '', // microformats 2 property
     17                'singular_name'   => __( 'Article', 'indieweb-post-kinds' ),
     18                'name'            => __( 'Articles', 'indieweb-post-kinds' ),
     19                'verb'            => ' ',
     20                'property'        => '',
    1221                'properties'      => array(),
    13                 'format'          => '', // Post Format that maps to this
     22                'format'          => '',
    1423                'description'     => __( 'traditional long form content: a post with an explicit title and body', 'indieweb-post-kinds' ),
    1524                'description_url' => 'http://indieweb.org/article',
    1625                'shortlink'       => 'b',
    17                 'title'           => true, // Should this kind have an explicit title
    18                 'show'            => true, // Show in Settings
     26                'title'           => true,
     27                'show'            => true,
    1928        )
    2029);
    … …  
    2332        'note',
    2433        array(
    25                 'singular_name'   => __( 'Note', 'indieweb-post-kinds' ), // Name for one instance of the kind
    26                 'name'            => __( 'Notes', 'indieweb-post-kinds' ), // General name for the kind plural
    27                 'verb'            => ' ', // The string for the verb or action (liked this)
    28                 'property'        => '', // microformats 2 property
     34                'singular_name'   => __( 'Note', 'indieweb-post-kinds' ),
     35                'name'            => __( 'Notes', 'indieweb-post-kinds' ),
     36                'verb'            => ' ',
     37                'property'        => '',
    2938                'properties'      => array(),
    30                 'format'          => 'aside', // Post Format that maps to this
     39                'format'          => 'aside',
    3140                'description'     => __( 'short content: a post or status update with just plain content and typically without a title', 'indieweb-post-kinds' ),
    3241                'description_url' => 'http://indieweb.org/note',
    3342                'shortlink'       => 't',
    34                 'title'           => false, // Should this kind have an explicit title
    35                 'show'            => false, // Show in Settings
     43                'title'           => false,
     44                'show'            => false,
    3645        )
    3746);
    … …  
    4049        'reply',
    4150        array(
    42                 'singular_name'   => __( 'Reply', 'indieweb-post-kinds' ), // Name for one instance of the kind
    43                 'name'            => __( 'Replies', 'indieweb-post-kinds' ), // General name for the kind plural
    44                 'verb'            => __( 'Replied to', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    45                 'property'        => 'in-reply-to', // microformats 2 property
     51                'singular_name'   => __( 'Reply', 'indieweb-post-kinds' ),
     52                'name'            => __( 'Replies', 'indieweb-post-kinds' ),
     53                'verb'            => __( 'Replied to', 'indieweb-post-kinds' ),
     54                'property'        => 'in-reply-to',
    4655                'properties'      => array(
    4756                        'in-reply-to' => array(
    … …  
    97106                        ),
    98107                ),
    99                 'format'          => 'link', // Post Format that maps to this
     108                'format'          => 'link',
    100109                'description'     => __( 'a reply to content typically on another site', 'indieweb-post-kinds' ),
    101110                'description_url' => 'http://indieweb.org/reply',
    102111                'shortlink'       => 't',
    103                 'title'           => false, // Should this kind have an explicit title
    104                 'show'            => true, // Show in Settings
     112                'title'           => false,
     113                'show'            => true,
    105114        )
    106115);
    … …  
    109118        'repost',
    110119        array(
    111                 'singular_name'   => __( 'Repost', 'indieweb-post-kinds' ), // Name for one instance of the kind
    112                 'name'            => __( 'Reposts', 'indieweb-post-kinds' ), // General name for the kind plural
    113                 'verb'            => __( 'Reposted', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    114                 'property'        => 'repost-of', // microformats 2 property
     120                'singular_name'   => __( 'Repost', 'indieweb-post-kinds' ),
     121                'name'            => __( 'Reposts', 'indieweb-post-kinds' ),
     122                'verb'            => __( 'Reposted', 'indieweb-post-kinds' ),
     123                'property'        => 'repost-of',
    115124                'properties'      => array(
    116125                        'repost-of' => array(
    … …  
    166175                        ),
    167176                ),
    168                 'format'          => '', // Post Format that maps to this
     177                'format'          => '',
    169178                'description'     => __( 'a complete reposting of content from another site', 'indieweb-post-kinds' ),
    170179                'description_url' => 'http://indieweb.org/repost',
    171180                'shortlink'       => 'b',
    172                 'title'           => true, // Should this kind have an explicit title
    173                 'show'            => true, // Show in Settings
     181                'title'           => true,
     182                'show'            => true,
    174183        )
    175184);
    … …  
    178187        'like',
    179188        array(
    180                 'singular_name'   => __( 'Like', 'indieweb-post-kinds' ), // Name for one instance of the kind
    181                 'name'            => __( 'Likes', 'indieweb-post-kinds' ), // General name for the kind plural
    182                 'verb'            => __( 'Liked', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    183                 'property'        => 'like-of', // microformats 2 property
     189                'singular_name'   => __( 'Like', 'indieweb-post-kinds' ),
     190                'name'            => __( 'Likes', 'indieweb-post-kinds' ),
     191                'verb'            => __( 'Liked', 'indieweb-post-kinds' ),
     192                'property'        => 'like-of',
    184193                'properties'      => array(
    185194                        'like-of' => array(
    … …  
    235244                        ),
    236245                ),
    237                 'format'          => 'link', // Post Format that maps to this
     246                'format'          => 'link',
    238247                'description'     => __( 'a way to pay compliments to the original post/poster of external content', 'indieweb-post-kinds' ),
    239248                'description_url' => 'http://indieweb.org/like',
    240249                'shortlink'       => 'f',
    241                 'title'           => false, // Should this kind have an explicit title
    242                 'show'            => true, // Show in Settings
     250                'title'           => false,
     251                'show'            => true,
    243252        )
    244253);
    … …  
    247256        'favorite',
    248257        array(
    249                 'singular_name'   => __( 'Favorite', 'indieweb-post-kinds' ), // Name for one instance of the kind
    250                 'name'            => __( 'Favorites', 'indieweb-post-kinds' ), // General name for the kind plural
    251                 'verb'            => __( 'Favorited', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    252                 'property'        => 'favorite-of', // microformats 2 property
     258                'singular_name'   => __( 'Favorite', 'indieweb-post-kinds' ),
     259                'name'            => __( 'Favorites', 'indieweb-post-kinds' ),
     260                'verb'            => __( 'Favorited', 'indieweb-post-kinds' ),
     261                'property'        => 'favorite-of',
    253262                'properties'      => array(
    254263                        'favorite-of' => array(
    … …  
    304313                        ),
    305314                ),
    306                 'format'          => 'link', // Post Format that maps to this
     315                'format'          => 'link',
    307316                'description'     => __( 'special to the author', 'indieweb-post-kinds' ),
    308317                'description_url' => 'http://indieweb.org/favorite',
    309318                'shortlink'       => 'f',
    310                 'title'           => false, // Should this kind have an explicit title
    311                 'show'            => true, // Show in Settings
     319                'title'           => false,
     320                'show'            => true,
    312321        )
    313322);
    … …  
    316325        'bookmark',
    317326        array(
    318                 'singular_name'   => __( 'Bookmark', 'indieweb-post-kinds' ), // Name for one instance of the kind
    319                 'name'            => __( 'Bookmarks', 'indieweb-post-kinds' ), // General name for the kind plural
    320                 'verb'            => __( 'Bookmarked', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    321                 'property'        => 'bookmark-of', // microformats 2 property
     327                'singular_name'   => __( 'Bookmark', 'indieweb-post-kinds' ),
     328                'name'            => __( 'Bookmarks', 'indieweb-post-kinds' ),
     329                'verb'            => __( 'Bookmarked', 'indieweb-post-kinds' ),
     330                'property'        => 'bookmark-of',
    322331                'properties'      => array(
    323332                        'bookmark-of' => array(
    … …  
    373382                        ),
    374383                ),
    375                 'format'          => 'link', // Post Format that maps to this
     384                'format'          => 'link',
    376385                'description'     => __( 'storing a link/bookmark for personal use or sharing with others', 'indieweb-post-kinds' ),
    377386                'description_url' => 'http://indieweb.org/bookmark',
    378387                'shortlink'       => 'h',
    379                 'title'           => false, // Should this kind have an explicit title
    380                 'show'            => true, // Show in Settings
     388                'title'           => false,
     389                'show'            => true,
    381390        )
    382391);
    … …  
    385394        'photo',
    386395        array(
    387                 'singular_name'   => __( 'Photo', 'indieweb-post-kinds' ), // Name for one instance of the kind
    388                 'name'            => __( 'Photos', 'indieweb-post-kinds' ), // General name for the kind plural
    389                 'verb'            => ' ', // The string for the verb or action (liked this)
    390                 'property'        => 'photo', // microformats 2 property
    391                 'format'          => 'image', // Post Format that maps to this
     396                'singular_name'   => __( 'Photo', 'indieweb-post-kinds' ),
     397                'name'            => __( 'Photos', 'indieweb-post-kinds' ),
     398                'verb'            => ' ',
     399                'property'        => 'photo',
     400                'format'          => 'image',
    392401                'description'     => __( 'a post with an embedded image/photo as its primary focus', 'indieweb-post-kinds' ),
    393402                'description_url' => 'http://indieweb.org/photo',
    394403                'shortlink'       => 'p',
    395                 'title'           => false, // Should this kind have an explicit title
    396                 'show'            => true, // Show in Settings
     404                'title'           => false,
     405                'show'            => true,
    397406        )
    398407);
    … …  
    401410        'video',
    402411        array(
    403                 'singular_name'   => __( 'Video', 'indieweb-post-kinds' ), // Name for one instance of the kind
    404                 'name'            => __( 'Videos', 'indieweb-post-kinds' ), // General name for the kind plural
    405                 'verb'            => ' ', // The string for the verb or action (liked this)
    406                 'property'        => 'video', // microformats 2 property
    407                 'format'          => 'video', // Post Format that maps to this
     412                'singular_name'   => __( 'Video', 'indieweb-post-kinds' ),
     413                'name'            => __( 'Videos', 'indieweb-post-kinds' ),
     414                'verb'            => ' ',
     415                'property'        => 'video',
     416                'format'          => 'video',
    408417                'description'     => __( 'a post with an embedded video as its primary focus', 'indieweb-post-kinds' ),
    409418                'description_url' => 'http://indieweb.org/video',
    410419                'shortlink'       => 'a',
    411                 'title'           => false, // Should this kind have an explicit title
    412                 'show'            => true, // Show in Settings
     420                'title'           => false,
     421                'show'            => true,
    413422        )
    414423);
    … …  
    417426        'audio',
    418427        array(
    419                 'singular_name'   => __( 'Audio', 'indieweb-post-kinds' ), // Name for one instance of the kind
    420                 'name'            => __( 'Audios', 'indieweb-post-kinds' ), // General name for the kind plural
    421                 'verb'            => ' ', // The string for the verb or action (liked this)
    422                 'property'        => 'audio', // microformats 2 property
    423                 'format'          => 'audio', // Post Format that maps to this
     428                'singular_name'   => __( 'Audio', 'indieweb-post-kinds' ),
     429                'name'            => __( 'Audios', 'indieweb-post-kinds' ),
     430                'verb'            => ' ',
     431                'property'        => 'audio',
     432                'format'          => 'audio',
    424433                'description'     => __( 'a post with an embedded audio file as its primary focus', 'indieweb-post-kinds' ),
    425434                'description_url' => 'http://indieweb.org/audio',
    426435                'shortlink'       => 'a',
    427                 'title'           => false, // Should this kind have an explicit title
    428                 'show'            => true, // Show in Settings
     436                'title'           => false,
     437                'show'            => true,
    429438        )
    430439);
    … …  
    433442        'tag',
    434443        array(
    435                 'singular_name'   => __( 'Tag', 'indieweb-post-kinds' ), // Name for one instance of the kind
    436                 'name'            => __( 'Tags', 'indieweb-post-kinds' ), // General name for the kind plural
    437                 'verb'            => __( 'Tagged', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    438                 'property'        => 'tag-of', // microformats 2 property
    439                 'format'          => 'link', // Post Format that maps to this
     444                'singular_name'   => __( 'Tag', 'indieweb-post-kinds' ),
     445                'name'            => __( 'Tags', 'indieweb-post-kinds' ),
     446                'verb'            => __( 'Tagged', 'indieweb-post-kinds' ),
     447                'property'        => 'tag-of',
     448                'format'          => 'link',
    440449                'description'     => __( 'allows you to tag a post as being of a specific category or tag, or for person tagging', 'indieweb-post-kinds' ),
    441450                'description_url' => 'http://indieweb.org/tag',
    442451                'shortlink'       => 'd',
    443                 'title'           => false, // Should this kind have an explicit title
    444                 'show'            => false, // Show in Settings
     452                'title'           => false,
     453                'show'            => false,
    445454        )
    446455);
    … …  
    449458        'rsvp',
    450459        array(
    451                 'singular_name'   => __( 'RSVP', 'indieweb-post-kinds' ), // Name for one instance of the kind
    452                 'name'            => __( 'RSVPs', 'indieweb-post-kinds' ), // General name for the kind plural
    453                 'verb'            => __( 'RSVPed', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    454                 'property'        => 'in-reply-to', // microformats 2 property
     460                'singular_name'   => __( 'RSVP', 'indieweb-post-kinds' ),
     461                'name'            => __( 'RSVPs', 'indieweb-post-kinds' ),
     462                'verb'            => __( 'RSVPed', 'indieweb-post-kinds' ),
     463                'property'        => 'in-reply-to',
    455464                'properties'      => array(
    456465                        'rsvp'        => array(
    … …  
    509518                        ),
    510519                ),
    511                 'format'          => 'link', // Post Format that maps to this
     520                'format'          => 'link',
    512521                'description'     => __( 'a specific type of reply regarding attendance of an event', 'indieweb-post-kinds' ),
    513522                'description_url' => 'http://indieweb.org/rsvp',
    514523                'shortlink'       => 't',
    515                 'title'           => false, // Should this kind have an explicit title
    516                 'show'            => true, // Show in Settings
     524                'title'           => false,
     525                'show'            => true,
    517526        )
    518527);
    … …  
    521530        'listen',
    522531        array(
    523                 'singular_name'   => __( 'Listen', 'indieweb-post-kinds' ), // Name for one instance of the kind
    524                 'name'            => __( 'Listens', 'indieweb-post-kinds' ), // General name for the kind plural
    525                 'verb'            => __( 'Listened', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    526                 'property'        => 'listen-of', // microformats 2 property
     532                'singular_name'   => __( 'Listen', 'indieweb-post-kinds' ),
     533                'name'            => __( 'Listens', 'indieweb-post-kinds' ),
     534                'verb'            => __( 'Listened', 'indieweb-post-kinds' ),
     535                'property'        => 'listen-of',
    527536                'properties'      => array(
    528537                        'listen-of' => array(
    … …  
    574583                        ),
    575584                ),
    576                 'format'          => 'audio', // Post Format that maps to this
     585                'format'          => 'audio',
    577586                'description'     => __( 'listening to audio; sometimes called a scrobble', 'indieweb-post-kinds' ),
    578587                'description_url' => 'http://indieweb.org/listen',
    579588                'shortlink'       => 'x',
    580                 'title'           => false, // Should this kind have an explicit title
    581                 'show'            => true, // Show in Settings
     589                'title'           => false,
     590                'show'            => true,
    582591        )
    583592);
    … …  
    586595        'watch',
    587596        array(
    588                 'singular_name'   => __( 'Watch', 'indieweb-post-kinds' ), // Name for one instance of the kind
    589                 'name'            => __( 'Watches', 'indieweb-post-kinds' ), // General name for the kind plural
    590                 'verb'            => __( 'Watched', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    591                 'property'        => 'watch-of', // microformats 2 property
     597                'singular_name'   => __( 'Watch', 'indieweb-post-kinds' ),
     598                'name'            => __( 'Watches', 'indieweb-post-kinds' ),
     599                'verb'            => __( 'Watched', 'indieweb-post-kinds' ),
     600                'property'        => 'watch-of',
    592601                'properties'      => array(
    593602                        'watch-of' => array(
    … …  
    639648                        ),
    640649                ),
    641                 'format'          => 'video', // Post Format that maps to this
     650                'format'          => 'video',
    642651                'description'     => __( 'watching a movie, television show, online video, play or other visual-based event', 'indieweb-post-kinds' ),
    643652                'description_url' => 'http://indieweb.org/watch',
    644653                'shortlink'       => 'x',
    645                 'title'           => false, // Should this kind have an explicit title
    646                 'show'            => true, // Show in Settings
     654                'title'           => false,
     655                'show'            => true,
    647656        )
    648657);
    … …  
    651660        'checkin',
    652661        array(
    653                 'singular_name'   => __( 'Checkin', 'indieweb-post-kinds' ), // Name for one instance of the kind
    654                 'name'            => __( 'Checkins', 'indieweb-post-kinds' ), // General name for the kind plural
    655                 'verb'            => __( 'Checked into', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    656                 'property'        => 'checkin', // microformats 2 property
     662                'singular_name'   => __( 'Checkin', 'indieweb-post-kinds' ),
     663                'name'            => __( 'Checkins', 'indieweb-post-kinds' ),
     664                'verb'            => __( 'Checked into', 'indieweb-post-kinds' ),
     665                'property'        => 'checkin',
    657666                'properties'      => array(
    658667                        'checkin' => array(
    … …  
    707716                        ),
    708717                ),
    709                 'format'          => 'status', // Post Format that maps to this
     718                'format'          => 'status',
    710719                'description'     => __( 'identifying you are at a particular geographic location', 'indieweb-post-kinds' ),
    711720                'description_url' => 'http://indieweb.org/checkin',
    712721                'shortlink'       => 'g',
    713                 'title'           => false, // Should this kind have an explicit title
    714                 'show'            => true, // Show in Settings
     722                'title'           => false,
     723                'show'            => true,
    715724        )
    716725);
    … …  
    719728        'wish',
    720729        array(
    721                 'singular_name'   => __( 'Wish', 'indieweb-post-kinds' ), // Name for one instance of the kind
    722                 'name'            => __( 'Wishes', 'indieweb-post-kinds' ), // General name for the kind plural
    723                 'verb'            => __( 'Wished', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    724                 'property'        => 'wish-of', // microformats 2 property
    725                 'format'          => 'link', // Post Format that maps to this
     730                'singular_name'   => __( 'Wish', 'indieweb-post-kinds' ),
     731                'name'            => __( 'Wishes', 'indieweb-post-kinds' ),
     732                'verb'            => __( 'Wished', 'indieweb-post-kinds' ),
     733                'property'        => 'wish-of',
     734                'format'          => 'link',
    726735                'description'     => __( 'a post indicating a desire/wish. The archive of which would be a wishlist, such as a gift registry or similar', 'indieweb-post-kinds' ),
    727736                'description_url' => '',
    728737                'shortlink'       => 'f',
    729                 'title'           => false, // Should this kind have an explicit title
    730                 'show'            => false, // Show in Settings
     738                'title'           => false,
     739                'show'            => false,
    731740        )
    732741);
    … …  
    735744        'play',
    736745        array(
    737                 'singular_name'   => __( 'Play', 'indieweb-post-kinds' ), // Name for one instance of the kind
    738                 'name'            => __( 'Playing', 'indieweb-post-kinds' ), // General name for the kind plural
    739                 'verb'            => __( 'Played', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    740                 'property'        => 'play-of', // microformats 2 property
    741                 'format'          => 'status', // Post Format that maps to this
     746                'singular_name'   => __( 'Play', 'indieweb-post-kinds' ),
     747                'name'            => __( 'Playing', 'indieweb-post-kinds' ),
     748                'verb'            => __( 'Played', 'indieweb-post-kinds' ),
     749                'property'        => 'play-of',
     750                'format'          => 'status',
    742751                'description'     => __( 'playing a game', 'indieweb-post-kinds' ),
    743752                'description_url' => 'https://indieweb.org/game_play',
    744753                'shortlink'       => 'x',
    745                 'title'           => false, // Should this kind have an explicit title
    746                 'show'            => true, // Show in Settings
     754                'title'           => false,
     755                'show'            => true,
    747756        )
    748757);
    … …  
    751760        'weather',
    752761        array(
    753                 'singular_name'   => __( 'Weather', 'indieweb-post-kinds' ), // Name for one instance of the kind
    754                 'name'            => __( 'Weather', 'indieweb-post-kinds' ), // General name for the kind plural
    755                 'verb'            => ' ', // The string for the verb or action (liked this)
    756                 'property'        => 'weather', // microformats 2 property
    757                 'format'          => 'status', // Post Format that maps to this
     762                'singular_name'   => __( 'Weather', 'indieweb-post-kinds' ),
     763                'name'            => __( 'Weather', 'indieweb-post-kinds' ),
     764                'verb'            => ' ',
     765                'property'        => 'weather',
     766                'format'          => 'status',
    758767                'description'     => __( 'current weather conditions', 'indieweb-post-kinds' ),
    759768                'description_url' => 'https://indieweb.org/weather',
    760769                'shortlink'       => 'u',
    761                 'title'           => false, // Should this kind have an explicit title
    762                 'show'            => false, // Show in Settings
     770                'title'           => false,
     771                'show'            => false,
    763772        )
    764773);
    … …  
    767776        'exercise',
    768777        array(
    769                 'singular_name'   => __( 'Exercise', 'indieweb-post-kinds' ), // Name for one instance of the kind
    770                 'name'            => __( 'Exercise', 'indieweb-post-kinds' ), // General name for the kind plural
    771                 'verb'            => __( 'Exercised', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    772                 'property'        => 'exercise', // microformats 2 property
    773                 'format'          => 'status', // Post Format that maps to this
     778                'singular_name'   => __( 'Exercise', 'indieweb-post-kinds' ),
     779                'name'            => __( 'Exercise', 'indieweb-post-kinds' ),
     780                'verb'            => __( 'Exercised', 'indieweb-post-kinds' ),
     781                'property'        => 'exercise',
     782                'format'          => 'status',
    774783                'description'     => __( 'some form of physical activity or workout (examples: walk, run, cycle, hike, yoga, etc.)', 'indieweb-post-kinds' ),
    775784                'description_url' => 'https://indieweb.org/exercise',
    776785                'shortlink'       => 'm',
    777                 'title'           => false, // Should this kind have an explicit title
    778                 'show'            => false, // Show in Settings
     786                'title'           => false,
     787                'show'            => false,
    779788        )
    780789);
    … …  
    783792        'trip',
    784793        array(
    785                 'singular_name'   => __( 'Trip', 'indieweb-post-kinds' ), // Name for one instance of the kind
    786                 'name'            => __( 'Trips', 'indieweb-post-kinds' ), // General name for the kind plural
    787                 'verb'            => __( 'Travelled', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    788                 'property'        => 'trip', // microformats 2 property
    789                 'format'          => '', // Post Format that maps to this
     794                'singular_name'   => __( 'Trip', 'indieweb-post-kinds' ),
     795                'name'            => __( 'Trips', 'indieweb-post-kinds' ),
     796                'verb'            => __( 'Travelled', 'indieweb-post-kinds' ),
     797                'property'        => 'trip',
     798                'format'          => '',
    790799                'description'     => __( 'represents a geographic journey', 'indieweb-post-kinds' ),
    791800                'description_url' => 'https://indieweb.org/trip',
    792801                'shortlink'       => 'e',
    793                 'title'           => false, // Should this kind have an explicit title
    794                 'show'            => false, // Show in Settings
     802                'title'           => false,
     803                'show'            => false,
    795804        )
    796805);
    … …  
    799808        'itinerary',
    800809        array(
    801                 'singular_name'   => __( 'Itinerary', 'indieweb-post-kinds' ), // Name for one instance of the kind
    802                 'name'            => __( 'Itineraries', 'indieweb-post-kinds' ), // General name for the kind plural
    803                 'verb'            => __( 'Travelled', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    804                 'property'        => 'itinerary', // microformats 2 property
    805                 'format'          => '', // Post Format that maps to this
     810                'singular_name'   => __( 'Itinerary', 'indieweb-post-kinds' ),
     811                'name'            => __( 'Itineraries', 'indieweb-post-kinds' ),
     812                'verb'            => __( 'Travelled', 'indieweb-post-kinds' ),
     813                'property'        => 'itinerary',
     814                'format'          => '',
    806815                'description'     => __( 'parts of a scheduled trip including transit by car, plane, train, etc.', 'indieweb-post-kinds' ),
    807816                'description_url' => 'https://indieweb.org/trip',
    808817                'shortlink'       => 'e',
    809                 'title'           => false, // Should this kind have an explicit title
    810                 'show'            => false, // Show in Settings
     818                'title'           => false,
     819                'show'            => false,
    811820        )
    812821);
    … …  
    816825        'eat',
    817826        array(
    818                 'singular_name'   => __( 'Eat', 'indieweb-post-kinds' ), // Name for one instance of the kind
    819                 'name'            => __( 'Eat', 'indieweb-post-kinds' ), // General name for the kind plural
    820                 'verb'            => __( 'Ate', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    821                 'property'        => 'ate', // microformats 2 property
    822                 'format'          => 'status', // Post Format that maps to this
     827                'singular_name'   => __( 'Eat', 'indieweb-post-kinds' ),
     828                'name'            => __( 'Eat', 'indieweb-post-kinds' ),
     829                'verb'            => __( 'Ate', 'indieweb-post-kinds' ),
     830                'property'        => 'ate',
     831                'format'          => 'status',
    823832                'description'     => __( 'what you are eating, perhaps for a food diary', 'indieweb-post-kinds' ),
    824833                'description_url' => 'https://indieweb.org/food',
    825834                'shortlink'       => 'u',
    826                 'title'           => false, // Should this kind have an explicit title
    827                 'show'            => true, // Show in Settings
     835                'title'           => false,
     836                'show'            => true,
    828837        )
    829838);
    … …  
    833842        'drink',
    834843        array(
    835                 'singular_name'   => __( 'Drink', 'indieweb-post-kinds' ), // Name for one instance of the kind
    836                 'name'            => __( 'Drinks', 'indieweb-post-kinds' ), // General name for the kind plural
    837                 'verb'            => __( 'Drank', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    838                 'property'        => 'drank', // microformats 2 property
    839                 'format'          => 'status', // Post Format that maps to this
     844                'singular_name'   => __( 'Drink', 'indieweb-post-kinds' ),
     845                'name'            => __( 'Drinks', 'indieweb-post-kinds' ),
     846                'verb'            => __( 'Drank', 'indieweb-post-kinds' ),
     847                'property'        => 'drank',
     848                'format'          => 'status',
    840849                'description'     => __( 'what you are drinking, perhaps for a food dairy', 'indieweb-post-kinds' ),
    841850                'description_url' => 'https://indieweb.org/food',
    842851                'shortlink'       => 'u',
    843                 'title'           => false, // Should this kind have an explicit title
    844                 'show'            => true, // Show in Settings
     852                'title'           => false,
     853                'show'            => true,
    845854        )
    846855);
    … …  
    850859        'follow',
    851860        array(
    852                 'singular_name'   => __( 'Follow', 'indieweb-post-kinds' ), // Name for one instance of the kind
    853                 'name'            => __( 'Follows', 'indieweb-post-kinds' ), // General name for the kind plural
    854                 'verb'            => __( 'Followed', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    855                 'property'        => 'follow-of', // microformats 2 property
    856                 'format'          => '', // Post Format that maps to this
     861                'singular_name'   => __( 'Follow', 'indieweb-post-kinds' ),
     862                'name'            => __( 'Follows', 'indieweb-post-kinds' ),
     863                'verb'            => __( 'Followed', 'indieweb-post-kinds' ),
     864                'property'        => 'follow-of',
     865                'format'          => '',
    857866                'description'     => __( 'indicating you are now following or subscribing to another person`s activities online', 'indieweb-post-kinds' ),
    858867                'description_url' => 'https://indieweb.org/follow',
    859868                'shortlink'       => 'f',
    860                 'title'           => false, // Should this kind have an explicit title
    861                 'show'            => false, // Show in Settings
     869                'title'           => false,
     870                'show'            => false,
    862871        )
    863872);
    … …  
    867876        'jam',
    868877        array(
    869                 'singular_name'   => __( 'Jam', 'indieweb-post-kinds' ), // Name for one instance of the kind
    870                 'name'            => __( 'Jams', 'indieweb-post-kinds' ), // General name for the kind plural
    871                 'verb'            => __( 'Listened to', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    872                 'property'        => 'jam-of', // microformats 2 property
    873                 'format'          => 'audio', // Post Format that maps to this
     878                'singular_name'   => __( 'Jam', 'indieweb-post-kinds' ),
     879                'name'            => __( 'Jams', 'indieweb-post-kinds' ),
     880                'verb'            => __( 'Listened to', 'indieweb-post-kinds' ),
     881                'property'        => 'jam-of',
     882                'format'          => 'audio',
    874883                'description'     => __( 'a particularly personally meaningful song (a listen with added emphasis)', 'indieweb-post-kinds' ),
    875884                'description_url' => 'https://indieweb.org/jam',
    876885                'shortlink'       => 'f',
    877                 'title'           => false, // Should this kind have an explicit title
    878                 'show'            => true, // Show in Settings
     886                'title'           => false,
     887                'show'            => true,
    879888        )
    880889);
    … …  
    884893        'read',
    885894        array(
    886                 'singular_name'   => __( 'Read', 'indieweb-post-kinds' ), // Name for one instance of the kind
    887                 'name'            => __( 'Reads', 'indieweb-post-kinds' ), // General name for the kind plural
    888                 'verb'            => __( 'Read', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    889                 'property'        => 'read-of', // microformats 2 property
    890                 'format'          => 'status', // Post Format that maps to this
     895                'singular_name'   => __( 'Read', 'indieweb-post-kinds' ),
     896                'name'            => __( 'Reads', 'indieweb-post-kinds' ),
     897                'verb'            => __( 'Read', 'indieweb-post-kinds' ),
     898                'property'        => 'read-of',
     899                'format'          => 'status',
    891900                'description'     => __( 'reading a book, magazine, newspaper, other physical document, or online post', 'indieweb-post-kinds' ),
    892901                'description_url' => 'https://indieweb.org/read',
    893902                'shortlink'       => 'x',
    894                 'title'           => false, // Should this kind have an explicit title
    895                 'show'            => true, // Show in Settings
     903                'title'           => false,
     904                'show'            => true,
    896905        )
    897906);
    … …  
    901910        'quote',
    902911        array(
    903                 'singular_name'   => __( 'Quote', 'indieweb-post-kinds' ), // Name for one instance of the kind
    904                 'name'            => __( 'Quotes', 'indieweb-post-kinds' ), // General name for the kind plural
    905                 'verb'            => __( 'Quoted', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    906                 'property'        => 'quotation-of', // microformats 2 property
    907                 'format'          => 'quote', // Post Format that maps to this
     912                'singular_name'   => __( 'Quote', 'indieweb-post-kinds' ),
     913                'name'            => __( 'Quotes', 'indieweb-post-kinds' ),
     914                'verb'            => __( 'Quoted', 'indieweb-post-kinds' ),
     915                'property'        => 'quotation-of',
     916                'format'          => 'quote',
    908917                'description'     => __( 'quoted content', 'indieweb-post-kinds' ),
    909918                'description_url' => 'https://indieweb.org/quote',
    910919                'shortlink'       => 't',
    911                 'title'           => false, // Should this kind have an explicit title
    912                 'show'            => true, // Show in Settings
     920                'title'           => false,
     921                'show'            => true,
    913922        )
    914923);
    … …  
    918927        'mood',
    919928        array(
    920                 'singular_name'   => __( 'Mood', 'indieweb-post-kinds' ), // Name for one instance of the kind
    921                 'name'            => __( 'Moods', 'indieweb-post-kinds' ), // General name for the kind plural
    922                 'verb'            => __( 'Felt', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    923                 'property'        => 'mood', // microformats 2 property
    924                 'format'          => 'status', // Post Format that maps to this
     929                'singular_name'   => __( 'Mood', 'indieweb-post-kinds' ),
     930                'name'            => __( 'Moods', 'indieweb-post-kinds' ),
     931                'verb'            => __( 'Felt', 'indieweb-post-kinds' ),
     932                'property'        => 'mood',
     933                'format'          => 'status',
    925934                'description'     => __( 'how you are feeling (example: happy, sad, indifferent, etc.)', 'indieweb-post-kinds' ),
    926935                'description_url' => 'https://indieweb.org/mood',
    927936                'shortlink'       => 't',
    928                 'title'           => false, // Should this kind have an explicit title
    929                 'show'            => false, // Show in Settings
     937                'title'           => false,
     938                'show'            => false,
    930939        )
    931940);
    … …  
    935944        'recipe',
    936945        array(
    937                 'singular_name'   => __( 'Recipe', 'indieweb-post-kinds' ), // Name for one instance of the kind
    938                 'name'            => __( 'Recipes', 'indieweb-post-kinds' ), // General name for the kind plural
    939                 'verb'            => __( 'Cooked', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    940                 'property'        => 'recipe', // microformats 2 property
    941                 'format'          => '', // Post Format that maps to this
     946                'singular_name'   => __( 'Recipe', 'indieweb-post-kinds' ),
     947                'name'            => __( 'Recipes', 'indieweb-post-kinds' ),
     948                'verb'            => __( 'Cooked', 'indieweb-post-kinds' ),
     949                'property'        => 'recipe',
     950                'format'          => '',
    942951                'description'     => __( 'list of ingredients and directions for making food or drink', 'indieweb-post-kinds' ),
    943952                'description_url' => 'https://indieweb.org/recipe',
    944953                'shortlink'       => 'b',
    945                 'title'           => true, // Should this kind have an explicit title
    946                 'show'            => false, // Show in Settings
     954                'title'           => true,
     955                'show'            => false,
    947956        )
    948957);
    … …  
    952961        'issue',
    953962        array(
    954                 'singular_name'   => __( 'Issue', 'indieweb-post-kinds' ), // Name for one instance of the kind
    955                 'name'            => __( 'Issues', 'indieweb-post-kinds' ), // General name for the kind plural
    956                 'verb'            => __( 'Filed an Issue', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    957                 'property'        => 'in-reply-to', // microformats 2 property
    958                 'format'          => '', // Post Format that maps to this
     963                'singular_name'   => __( 'Issue', 'indieweb-post-kinds' ),
     964                'name'            => __( 'Issues', 'indieweb-post-kinds' ),
     965                'verb'            => __( 'Filed an Issue', 'indieweb-post-kinds' ),
     966                'property'        => 'in-reply-to',
     967                'format'          => '',
    959968                'description'     => __( 'Issue is a special kind of article post that is a reply to typically some source code, though potentially anything at a source control repository.', 'indieweb-post-kinds' ),
    960969                'description_url' => 'https://indieweb.org/issue',
    961970                'shortlink'       => 't',
    962                 'title'           => true, // Should this kind have an explicit title
    963                 'show'            => true, // Show in Settings
     971                'title'           => true,
     972                'show'            => true,
    964973        )
    965974);
    … …  
    969978        'question',
    970979        array(
    971                 'singular_name'   => __( 'Question', 'indieweb-post-kinds' ), // Name for one instance of the kind
    972                 'name'            => __( 'Questions', 'indieweb-post-kinds' ), // General name for the kind plural
    973                 'verb'            => __( 'Asked a question', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    974                 'property'        => 'question', // microformats 2 property
    975                 'format'          => '', // Post Format that maps to this
     980                'singular_name'   => __( 'Question', 'indieweb-post-kinds' ),
     981                'name'            => __( 'Questions', 'indieweb-post-kinds' ),
     982                'verb'            => __( 'Asked a question', 'indieweb-post-kinds' ),
     983                'property'        => 'question',
     984                'format'          => '',
    976985                'description'     => __( 'Question is a post type for soliciting answer replies, which are then typically up/down voted by others and then displayed underneath the question post ordered by highest positive vote count rather than time ordered.', 'indieweb-post-kinds' ),
    977986                'description_url' => 'https://indieweb.org/question',
    978987                'shortlink'       => 'q',
    979                 'title'           => false, // Should this kind have an explicit title
    980                 'show'            => false, // Show in Settings
     988                'title'           => false,
     989                'show'            => false,
    981990        )
    982991);
    … …  
    986995        'sleep',
    987996        array(
    988                 'singular_name'   => __( 'Sleep', 'indieweb-post-kinds' ), // Name for one instance of the kind
    989                 'name'            => __( 'Sleeps', 'indieweb-post-kinds' ), // General name for the kind plural
    990                 'verb'            => __( 'Slept', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    991                 'property'        => 'sleep', // microformats 2 property
    992                 'format'          => '', // Post Format that maps to this
     997                'singular_name'   => __( 'Sleep', 'indieweb-post-kinds' ),
     998                'name'            => __( 'Sleeps', 'indieweb-post-kinds' ),
     999                'verb'            => __( 'Slept', 'indieweb-post-kinds' ),
     1000                'property'        => 'sleep',
     1001                'format'          => '',
    9931002                'description'     => __( 'Sleep is a passive metrics post type that indicates how much time (and often a graph of how deeply) a person has slept.', 'indieweb-post-kinds' ),
    9941003                'description_url' => 'https://indieweb.org/sleep',
    9951004                'shortlink'       => 'm',
    996                 'title'           => false, // Should this kind have an explicit title
    997                 'show'            => false, // Show in Settings
     1005                'title'           => false,
     1006                'show'            => false,
    9981007        )
    9991008);
    … …  
    10031012        'event',
    10041013        array(
    1005                 'singular_name'   => __( 'Event', 'indieweb-post-kinds' ), // Name for one instance of the kind
    1006                 'name'            => __( 'Events', 'indieweb-post-kinds' ), // General name for the kind plural
    1007                 'verb'            => __( 'Planned', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    1008                 'property'        => 'event', // microformats 2 property
    1009                 'format'          => '', // Post Format that maps to this
     1014                'singular_name'   => __( 'Event', 'indieweb-post-kinds' ),
     1015                'name'            => __( 'Events', 'indieweb-post-kinds' ),
     1016                'verb'            => __( 'Planned', 'indieweb-post-kinds' ),
     1017                'property'        => 'event',
     1018                'format'          => '',
    10101019                'description'     => __( 'An event is a type of post that in addition to a post name (event title) has a start datetime (likely end datetime), and a location.', 'indieweb-post-kinds' ),
    10111020                'description_url' => 'https://indieweb.org/event',
    10121021                'shortlink'       => 'e',
    1013                 'title'           => true, // Should this kind have an explicit title
    1014                 'show'            => false, // Show in Settings
     1022                'title'           => true,
     1023                'show'            => false,
    10151024        )
    10161025);
    … …  
    10191028        'review',
    10201029        array(
    1021                 'singular_name'   => __( 'Review', 'indieweb-post-kinds' ), // Name for one instance of the kind
    1022                 'name'            => __( 'Reviews', 'indieweb-post-kinds' ), // General name for the kind plural
    1023                 'verb'            => __( 'Reviewed', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    1024                 'property'        => 'review-of', // microformats 2 property
    1025                 'format'          => '', // Post Format that maps to this
     1030                'singular_name'   => __( 'Review', 'indieweb-post-kinds' ),
     1031                'name'            => __( 'Reviews', 'indieweb-post-kinds' ),
     1032                'verb'            => __( 'Reviewed', 'indieweb-post-kinds' ),
     1033                'property'        => 'review-of',
     1034                'format'          => '',
    10261035                'description'     => __( 'A review is a post evaluating a product or service, usually involving a written description, sometimes with summary numerical evaluations, also known as just a rating.', 'indieweb-post-kinds' ),
    10271036                'description_url' => 'https://indieweb.org/review',
    10281037                'shortlink'       => 't',
    1029                 'title'           => false, // Should this kind have an explicit title
    1030                 'show'            => true, // Show in Settings
     1038                'title'           => false,
     1039                'show'            => true,
    10311040        )
    10321041);
    … …  
    10361045        'acquisition',
    10371046        array(
    1038                 'singular_name'   => __( 'Acquisition', 'indieweb-post-kinds' ), // Name for one instance of the kind
    1039                 'name'            => __( 'Acquisitions', 'indieweb-post-kinds' ), // General name for the kind plural
    1040                 'verb'            => __( 'Acquired', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    1041                 'property'        => 'acquired-of', // microformats 2 property
    1042                 'format'          => 'status', // Post Format that maps to this
     1047                'singular_name'   => __( 'Acquisition', 'indieweb-post-kinds' ),
     1048                'name'            => __( 'Acquisitions', 'indieweb-post-kinds' ),
     1049                'verb'            => __( 'Acquired', 'indieweb-post-kinds' ),
     1050                'property'        => 'acquired-of',
     1051                'format'          => 'status',
    10431052                'description'     => __( 'Purchases, gifts, found things, or objects donated', 'indieweb-post-kinds' ),
    10441053                'description_url' => 'http://indieweb.org/acquisition',
    10451054                'shortlink'       => 'f',
    1046                 'title'           => false, // Should this kind have an explicit title
    1047                 'show'            => false, // Show in Settings
     1055                'title'           => false,
     1056                'show'            => false,
    10481057        )
    10491058);
    … …  
    10521061        'craft',
    10531062        array(
    1054                 'singular_name'   => __( 'Craft', 'indieweb-post-kinds' ), // Name for one instance of the kind
    1055                 'name'            => __( 'Crafts', 'indieweb-post-kinds' ), // General name for the kind plural
    1056                 'verb'            => __( 'Crafted', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    1057                 'property'        => 'craft-of', // microformats 2 property
    1058                 'format'          => 'status', // Post Format that maps to this
     1063                'singular_name'   => __( 'Craft', 'indieweb-post-kinds' ),
     1064                'name'            => __( 'Crafts', 'indieweb-post-kinds' ),
     1065                'verb'            => __( 'Crafted', 'indieweb-post-kinds' ),
     1066                'property'        => 'craft-of',
     1067                'format'          => 'status',
    10591068                'description'     => __( 'Activities like knitting, crocheting, cross stitch, wood working, restoration, 3d printing...the activity of building something.', 'indieweb-post-kinds' ),
    10601069                'description_url' => 'https://indieweb.org/crafts',
    10611070                'shortlink'       => 'x',
    1062                 'title'           => false, // Should this kind have an explicit title
    1063                 'show'            => true, // Show in Settings
    1064         )
    1065 );
     1071                'title'           => false,
     1072                'show'            => true,
     1073        )
     1074);
  • indieweb-post-kinds/tags/4.0.0/includes/time-functions.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Time Functions
     3 * Global date and time functions.
    44 *
    5  * Global Scoped Functions for Handling Time.
     5 * The generic helpers moved to Kind_Time in 4.0.0; their global names remain
     6 * here as deprecated wrappers.
    67 *
     8 * @package Post_Kinds
     9 * @since   3.0.0
    710 */
    811
     12defined( 'ABSPATH' ) || exit;
     13
     14if ( ! function_exists( 'kind_safe_datetime' ) ) {
     15        /**
     16         * Creates a date from a value without throwing on invalid input.
     17         *
     18         * Dates come from post meta and Micropub requests, where a malformed value
     19         * would otherwise throw an uncaught exception and break the page.
     20         *
     21         * @since 4.0.0
     22         *
     23         * @param DateTimeInterface|string $value     Date, or a date string.
     24         * @param DateTimeZone|null        $timezone  Optional. Timezone for strings without one.
     25         * @param bool                     $immutable Optional. Whether to return a DateTimeImmutable. Default true.
     26         * @return DateTimeImmutable|DateTime|false The date, or false if the value is not a valid date.
     27         */
     28        function kind_safe_datetime( $value, $timezone = null, $immutable = true ) {
     29                if ( $value instanceof DateTimeInterface ) {
     30                        if ( $immutable ) {
     31                                return $value instanceof DateTimeImmutable ? $value : DateTimeImmutable::createFromMutable( $value );
     32                        }
     33                        return $value instanceof DateTime ? $value : DateTime::createFromImmutable( $value );
     34                }
     35                if ( ! is_string( $value ) || '' === trim( $value ) ) {
     36                        return false;
     37                }
     38                try {
     39                        return $immutable ? new DateTimeImmutable( $value, $timezone ) : new DateTime( $value, $timezone );
     40                } catch ( Exception $e ) {
     41                        return false;
     42                }
     43        }
     44}
     45
     46if ( ! function_exists( 'kind_safe_interval' ) ) {
     47        /**
     48         * Creates a duration from an ISO 8601 duration without throwing on invalid input.
     49         *
     50         * @since 4.0.0
     51         *
     52         * @param DateInterval|string $value Duration, or an ISO 8601 duration such as PT3M30S.
     53         * @return DateInterval|false The duration, or false if the value is not a valid duration.
     54         */
     55        function kind_safe_interval( $value ) {
     56                if ( $value instanceof DateInterval ) {
     57                        return $value;
     58                }
     59                if ( ! is_string( $value ) || '' === trim( $value ) ) {
     60                        return false;
     61                }
     62                try {
     63                        return new DateInterval( $value );
     64                } catch ( Exception $e ) {
     65                        return false;
     66                }
     67        }
     68}
     69
    970if ( ! function_exists( 'tz_seconds_to_offset' ) ) {
     71        /**
     72         * Converts a UTC offset in seconds to an offset string.
     73         *
     74         * @since 2.3.0
     75         *
     76         * @param int $seconds Offset in seconds.
     77         * @return string Offset such as '-05:00'.
     78         */
    1079        function tz_seconds_to_offset( $seconds ) {
    1180                return ( $seconds < 0 ? '-' : '+' ) . sprintf( '%02d:%02d', abs( $seconds / 60 / 60 ), abs( $seconds / 60 ) % 60 );
    … …  
    1483
    1584if ( ! function_exists( 'tz_offset_to_seconds' ) ) {
     85        /**
     86         * Converts an offset string to a UTC offset in seconds.
     87         *
     88         * @since 2.3.0
     89         *
     90         * @param string $offset Offset such as '-05:00' or '-0500'.
     91         * @return int Offset in seconds, or 0 if the string is not an offset.
     92         */
    1693        function tz_offset_to_seconds( $offset ) {
    1794                if ( preg_match( '/([+-])(\d{2}):?(\d{2})/', $offset, $match ) ) {
    18                         $sign = ( '-' ? -1 : 1 === $match[1] );
     95                        $sign = ( '-' === $match[1] ) ? -1 : 1;
    1996                        return ( ( $match[2] * 60 * 60 ) + ( $match[3] * 60 ) ) * $sign;
    2097                } else {
    … …  
    25102
    26103if ( ! function_exists( 'tz_seconds_to_timezone' ) ) {
     104        /**
     105         * Converts a UTC offset in seconds to a time zone.
     106         *
     107         * @since 3.2.2
     108         *
     109         * @param int $seconds Offset in seconds.
     110         * @return DateTimeZone|false The time zone, or false if it cannot be created.
     111         */
    27112        function tz_seconds_to_timezone( $seconds ) {
    28                 if ( version_compare( phpversion(), '5.5.10', '<' ) ) {
    29                         return timezone_name_from_abbr( '', $seconds, 0 );
    30                 }
    31113                if ( 0 !== $seconds ) {
    32114                        $tz = timezone_open( tz_seconds_to_offset( $seconds ) );
    … …  
    40122
    41123if ( ! function_exists( 'tz_timezone_to_seconds' ) ) {
     124        /**
     125         * Returns a time zone's UTC offset in seconds.
     126         *
     127         * @since 3.2.2
     128         *
     129         * @param string $timezone Time zone name or offset string.
     130         * @return int|false Offset in seconds, or false if the time zone is not valid.
     131         */
    42132        function tz_timezone_to_seconds( $timezone ) {
    43133                $tz = timezone_open( $timezone );
    … …  
    51141if ( ! function_exists( 'get_gmt_offsets' ) ) {
    52142
     143        /**
     144         * Returns the UTC offsets currently in use by any time zone, sorted.
     145         *
     146         * The list is built from every time zone, so it is kept in the object
     147         * cache for the day: for the request, and across requests when the cache
     148         * is persistent. Daylight saving changes it at most a few times a year.
     149         *
     150         * @since 3.0.0
     151         *
     152         * @return string[] Offsets such as '-05:00'.
     153         */
    53154        function get_gmt_offsets() {
     155                $key = 'gmt_offsets_' . gmdate( 'Y-m-d' );
     156                $o   = wp_cache_get( $key, 'post-kinds' );
     157                if ( is_array( $o ) ) {
     158                        return $o;
     159                }
    54160                $o       = array();
    55161                $t_zones = timezone_identifiers_list();
    56162                foreach ( $t_zones as $a ) {
    57                         $t = '';
    58163                        try {
    59                                 // this throws exception for 'US/Pacific-New'
     164                                // Some identifiers, such as 'US/Pacific-New', throw on some PHP versions.
    60165                                $zone    = new DateTimeZone( $a );
    61166                                $seconds = $zone->getOffset( new DateTime( 'now', $zone ) );
    62167                                $o[]     = tz_seconds_to_offset( $seconds );
    63168                        } catch ( Exception $e ) {
    64                                 die( 'Exception : ' . esc_html( $e->getMessage() ) . '<br />' );
    65                                 // what to do in catch ? , nothing just relax
     169                                // Skip the zone rather than fail.
     170                                continue;
    66171                        }
    67172                }
    68173                $o = array_unique( $o );
    69174                asort( $o );
     175                wp_cache_set( $key, $o, 'post-kinds', DAY_IN_SECONDS );
    70176                return $o;
    71177        }
    … …  
    73179
    74180if ( ! function_exists( 'get_default_offset' ) ) {
    75         // Gets the default offset
     181        /**
     182         * Returns the site's UTC offset, from its gmt_offset option.
     183         *
     184         * @since 3.0.0
     185         *
     186         * @return string Offset such as '-05:00'.
     187         */
    76188        function get_default_offset() {
    77189                $tz_seconds = get_option( 'gmt_offset' ) * 3600;
    … …  
    81193
    82194if ( ! function_exists( 'build_iso8601_time' ) ) {
    83         // Turns individual pieces of a date and time into a single ISO8601 string
     195        /**
     196         * Joins a date, time and offset into an ISO 8601 string.
     197         *
     198         * @since 3.0.0
     199         *
     200         * @param string $date   Date in Y-m-d format.
     201         * @param string $time   Time in H:i:s format.
     202         * @param string $offset Offset such as '-05:00'. Empty for the site's offset.
     203         * @return string The date and time, or an empty string if both are empty.
     204         */
    84205        function build_iso8601_time( $date, $time, $offset ) {
    85206                if ( empty( $date ) && empty( $time ) ) {
    … …  
    94215
    95216if ( ! function_exists( 'build_iso8601_duration' ) ) {
    96         // Given an array with the pieces of a duration build an ISO8601 duration
     217        /**
     218         * Builds an ISO 8601 duration from its pieces.
     219         *
     220         * @since 3.0.0
     221         *
     222         * @param int[] $values Years (Y), months (M), days (D), hours (H), minutes (I) and seconds (S).
     223         * @return string The duration, or an empty string if every piece is empty.
     224         */
    97225        function build_iso8601_duration( $values ) {
    98226                $values = array_filter( $values );
    … …  
    128256
    129257if ( ! function_exists( 'calculate_duration' ) ) {
    130         // Given two ISO8601 time strings return a DateInterval Object
     258        /**
     259         * Returns the duration between two dates.
     260         *
     261         * @since 2.5.0
     262         * @since 4.0.0 Also accepts DateTimeInterface objects, such as Kind_Post::get( 'start' ) returns.
     263         *
     264         * @param DateTimeInterface|string $start_string Start, as a date or a string in Y-m-d\TH:i:sP format.
     265         * @param DateTimeInterface|string $end_string   End, as a date or a string in Y-m-d\TH:i:sP format.
     266         * @return DateInterval|false The duration, or false if either date is not valid or they are the same.
     267         */
    131268        function calculate_duration( $start_string, $end_string ) {
    132                 $start = array();
    133                 $end   = array();
    134                 if ( ! is_string( $start_string ) || ! is_string( $end_string ) ) {
     269                $dates = array();
     270                foreach ( array( $start_string, $end_string ) as $value ) {
     271                        if ( is_string( $value ) ) {
     272                                // Only the full format: looser parsing reads stray text, such as a single letter, as a date.
     273                                $value = date_create_immutable_from_format( 'Y-m-d\TH:i:sP', $value );
     274                        }
     275                        if ( ! $value instanceof DateTimeInterface ) {
     276                                return false;
     277                        }
     278                        $dates[] = $value;
     279                }
     280                list( $start, $end ) = $dates;
     281                if ( $start == $end ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Compares the moments, not the objects.
    135282                        return false;
    136283                }
    137                 if ( $start_string === $end_string ) {
    138                         return false;
    139                 }
    140                 $start = date_create_from_format( 'Y-m-d\TH:i:sP', $start_string );
    141                 $end   = date_create_from_format( 'Y-m-d\TH:i:sP', $end_string );
    142                 if ( ( $start instanceof DateTime ) && ( $end instanceof DateTime ) ) {
    143                         $duration = $start->diff( $end );
    144                         return $duration;
    145                 }
    146                 return false;
    147         }
    148 }
    149 
    150 if ( ! function_exists( 'seconds_to_iso8601' ) ) {
    151         function seconds_to_iso8601( $second ) {
    152                 $h   = intval( $second / 3600 );
    153                 $m   = intval( ( $second - $h * 3600 ) / 60 );
    154                 $s   = $second - ( $h * 3600 + $m * 60 );
    155                 $ret = 'PT';
    156                 if ( $h ) {
    157                         $ret .= $h . 'H';
    158                 }
    159                 if ( $m ) {
    160                         $ret .= $m . 'M';
    161                 }
    162                 if ( ( ! $h && ! $m ) || $s ) {
    163                         $ret .= $s . 'S';
    164                 }
    165                 return $ret;
     284                return $start->diff( $end );
    166285        }
    167286}
    168287
    169288if ( ! function_exists( 'date_interval_to_iso8601' ) ) {
    170 
    171         // Return a date interval as an ISO8601 string
     289        /**
     290         * Converts a duration to an ISO 8601 duration string.
     291         *
     292         * @since 3.0.0
     293         *
     294         * @param \DateInterval $interval The duration.
     295         * @return string The duration, such as 'PT3M30S'.
     296         */
    172297        function date_interval_to_iso8601( \DateInterval $interval ) {
    173298                // Reading all non-zero date parts.
    … …  
    202327}
    203328
    204 function display_formatted_datetime( $date ) {
    205         if ( is_string( $date ) ) {
    206                 $date = new DateTimeImmutable( $date );
    207         }
    208 
    209         return $date->format( get_option( 'date_format' ) ) . ' ' . $date->format( get_option( 'time_format' ) );
    210 }
    211 
    212 
    213 /**
    214  * Function to divide a datetime into an array for use in a field
    215  *
    216  * @access public
    217  *
    218  * @param DateTime $datetime
    219  * @return array {
    220  *  @type string $date Date in Y-m-d format.
    221  *  @type string $time Time in H:i:s format.
    222  *  @type DateTimeZone $timezone Timezone object.
    223  * }
    224  */
    225 function divide_datetime( $datetime ) {
    226         if ( ! $datetime ) {
    227                 return false;
    228         }
    229 
    230         if ( is_string( $datetime ) ) {
    231                 $datetime = new DateTime( $datetime );
    232         }
    233 
    234         if ( ! $datetime ) {
    235                 return false;
    236         }
    237 
    238         $time         = array();
    239         $time['date'] = $datetime->format( 'Y-m-d' );
    240         if ( '0000-01-01' === $time['date'] ) {
    241                 $time['date'] = '';
    242         }
    243         $time['time']   = $datetime->format( 'H:i:s' );
    244         $time['offset'] = get_datetime_offset( $datetime );
    245         $time['class']  = get_class( $datetime );
    246         return array_filter( $time );
    247 }
    248 
    249 /**
    250  * Function to build a datetime from individual pieces
    251  *
    252  * @access public
    253  *
    254  * @param string $date Date in Y-m-d format.
    255  * @param string $time Time in H:i:s format.
    256  * @param DateTimeZone $timezone Timezone object.
    257  *
    258  * @return DateTimeImmutable|false DateTime object or false if not valid
    259  */
    260 function build_datetime( $date, $time, $offset = null ) {
    261         if ( empty( $date ) && empty( $time ) ) {
    262                 return false;
    263         }
    264         if ( is_string( $offset ) ) {
    265                 $timezone = timezone_open( $offset );
    266         } elseif ( $offset instanceof DateTimeZone ) {
    267                 $timezone = $offset;
    268         } else {
    269                 return false;
    270         }
    271         if ( ! $timezone ) {
    272                 $timezone = wp_timezone();
    273         }
    274         return date_create_immutable_from_format( 'Y-m-d\TH:i:sP', $date . 'T' . $time, $timezone );
    275 }
    276 
    277 /**
    278  * Return a formatted offset from a datetime object
    279  *
    280  * @access public
    281  *
    282  * @param DateTime $datetime DateTime object or if not passed set to now and site timezone
    283  *
    284  * @return string|false Formatted offset or false if not valid
    285  */
    286 function get_datetime_offset( $datetime = null ) {
    287         if ( ! $datetime ) {
    288                 $datetime = new DateTimeImmutable( 'now', wp_timezone() );
    289         }
    290         $seconds = $datetime->getOffset();
    291         if ( false === $seconds ) {
    292                 return false;
    293         }
    294         return ( $seconds < 0 ? '-' : '+' ) . sprintf( '%02d:%02d', abs( $seconds / 60 / 60 ), abs( $seconds / 60 ) % 60 );
    295 }
    296 
    297 
    298 // Given an ISO8601 duration return an array with the piece otherwise 0 duration.
    299 function divide_interval( $interval ) {
    300         $default = array(
    301                 'Y' => 0,
    302                 'M' => 0,
    303                 'D' => 0,
    304                 'H' => 0,
    305                 'I' => 0,
    306                 'S' => 0,
    307         );
    308         if ( ! $interval ) {
    309                 return $default;
    310         }
    311         if ( is_string( $interval ) && ! empty( $interval ) ) {
    312                 try {
    313                         $interval = new DateInterval( $interval );
    314                 } catch ( \Exception $e ) {
    315                         return $default;
    316                 }
    317         }
    318         // Reading all non-zero date parts.
    319         $return = array(
    320                 'Y' => $interval->y,
    321                 'M' => $interval->m,
    322                 'D' => $interval->d,
    323                 'H' => $interval->h,
    324                 'I' => $interval->i,
    325                 'S' => $interval->s,
    326         );
    327         return wp_parse_args( $return, $default );
    328 }
    329 
    330 
    331 // Given an array with the pieces of a duration build an ISO8601 duration
    332 function build_interval( $values ) {
    333         $date = wp_array_slice_assoc( $values, array( 'Y', 'M', 'D' ) );
    334         $time = wp_array_slice_assoc( $values, array( 'H', 'I', 'S' ) );
    335         if ( ! $date || ! $time ) {
    336                 return '';
    337         }
    338         $spec = 'P';
    339         // Adding each part to the spec-string.
    340         foreach ( $date as $key => $value ) {
    341                 $spec .= $value . $key;
    342         }
    343         if ( count( $time ) > 0 ) {
    344                 $spec .= 'T';
    345                 foreach ( $time as $key => $value ) {
    346                         if ( 'I' === $key ) {
    347                                 $spec .= $value . 'M';
    348                         } else {
    349                                 $spec .= $value . $key;
    350                         }
    351                 }
    352         }
    353         return $spec;
    354 }
     329if ( ! function_exists( 'display_formatted_datetime' ) ) {
     330        /**
     331         * Deprecated wrapper; see Kind_Time::display_formatted_datetime().
     332         *
     333         * @since 3.2.0
     334         * @deprecated 4.0.0 Use Kind_Time::display_formatted_datetime(). Will be removed in 5.0.0.
     335         *
     336         * @param DateTimeInterface|string $date Date, or a string kind_safe_datetime() accepts.
     337         * @return string The formatted date and time, or an empty string if the date is not valid.
     338         */
     339        function display_formatted_datetime( $date ) {
     340                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::display_formatted_datetime()' );
     341                return Kind_Time::display_formatted_datetime( $date );
     342        }
     343}
     344
     345if ( ! function_exists( 'divide_datetime' ) ) {
     346        /**
     347         * Deprecated wrapper; see Kind_Time::divide_datetime().
     348         *
     349         * @since 3.3.4
     350         * @deprecated 4.0.0 Use Kind_Time::divide_datetime(). Will be removed in 5.0.0.
     351         *
     352         * @param DateTimeInterface|string $datetime Date, or a string kind_safe_datetime() accepts.
     353         * @return array|false The date's parts. See Kind_Time::divide_datetime().
     354         */
     355        function divide_datetime( $datetime ) {
     356                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::divide_datetime()' );
     357                return Kind_Time::divide_datetime( $datetime );
     358        }
     359}
     360
     361if ( ! function_exists( 'build_datetime' ) ) {
     362        /**
     363         * Deprecated wrapper; see Kind_Time::build_datetime().
     364         *
     365         * @since 3.3.4
     366         * @deprecated 4.0.0 Use Kind_Time::build_datetime(). Will be removed in 5.0.0.
     367         *
     368         * @param string                   $date   Date in Y-m-d format.
     369         * @param string                   $time   Time in H:i or H:i:s format.
     370         * @param string|DateTimeZone|null $offset Optional. Offset or time zone. Default the site's time zone.
     371         * @return DateTimeImmutable|false The date, or false if it is not valid.
     372         */
     373        function build_datetime( $date, $time, $offset = null ) {
     374                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::build_datetime()' );
     375                return Kind_Time::build_datetime( $date, $time, $offset );
     376        }
     377}
     378
     379if ( ! function_exists( 'get_datetime_offset' ) ) {
     380        /**
     381         * Deprecated wrapper; see Kind_Time::get_datetime_offset().
     382         *
     383         * @since 3.3.4
     384         * @deprecated 4.0.0 Use Kind_Time::get_datetime_offset(). Will be removed in 5.0.0.
     385         *
     386         * @param DateTimeInterface|null $datetime Optional. Date. Default now, in the site's time zone.
     387         * @return string|false The offset, or false if it cannot be read.
     388         */
     389        function get_datetime_offset( $datetime = null ) {
     390                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::get_datetime_offset()' );
     391                return Kind_Time::get_datetime_offset( $datetime );
     392        }
     393}
     394
     395if ( ! function_exists( 'divide_interval' ) ) {
     396        /**
     397         * Deprecated wrapper; see Kind_Time::divide_interval().
     398         *
     399         * @since 3.3.4
     400         * @deprecated 4.0.0 Use Kind_Time::divide_interval(). Will be removed in 5.0.0.
     401         *
     402         * @param DateInterval|string|null $interval Duration, or an ISO 8601 duration string.
     403         * @return int[] The duration's parts. See Kind_Time::divide_interval().
     404         */
     405        function divide_interval( $interval ) {
     406                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::divide_interval()' );
     407                return Kind_Time::divide_interval( $interval );
     408        }
     409}
     410
     411if ( ! function_exists( 'build_interval' ) ) {
     412        /**
     413         * Deprecated wrapper; see Kind_Time::build_interval().
     414         *
     415         * @since 3.3.4
     416         * @deprecated 4.0.0 Use Kind_Time::build_interval(). Will be removed in 5.0.0.
     417         *
     418         * @param int[] $values Years (Y), months (M), days (D), hours (H), minutes (I) and seconds (S).
     419         * @return string The duration, or an empty string if every piece is 0.
     420         */
     421        function build_interval( $values ) {
     422                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::build_interval()' );
     423                return Kind_Time::build_interval( $values );
     424        }
     425}
  • indieweb-post-kinds/tags/4.0.0/indieweb-post-kinds.php

    r3067251 r3738687  
    44 *
    55 * @link    http://indieweb.org/Post_Kinds_Plugin
    6  * @package Post Kinds
     6 * @package Post_Kinds
    77 * Plugin Name: Post Kinds
    88 * Plugin URI: https://wordpress.org/plugins/indieweb-post-kinds/
    99 * Description: Ever want to reply to someone else's post with a post on your own site? Or to "like" someone else's post, but with your own site?
    10  * Version: 3.7.3
     10 * Version: 4.0.0
     11 * Requires at least: 6.2
     12 * Requires PHP: 7.4
     13 * License: GPLv2 or later
     14 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1115 * Author: David Shanske
    1216 * Author URI: https://david.shanske.com
    … …  
    1418 * Domain Path:  /languages
    1519 */
     20
     21defined( 'ABSPATH' ) || exit;
    1622
    1723if ( ! defined( 'POST_KINDS_KSES' ) ) {
    … …  
    4046add_action( 'upgrader_process_complete', array( 'Post_Kinds_Plugin', 'upgrader_process_complete' ), 10, 2 );
    4147
    42 if ( ! file_exists( plugin_dir_path( __FILE__ ) . 'lib/parse-this/parse-this.php' ) ) {
    43         add_action( 'admin_notices', array( 'Post_Kinds_Plugin', 'parse_this_error' ) );
    44 }
    4548
    4649if ( Post_Kinds_Plugin::show_editor_error() ) {
    … …  
    5154add_action( 'init', array( 'Post_Kinds_Plugin', 'init' ) );
    5255
     56/**
     57 * Loads the plugin, its assets and its global functions.
     58 */
    5359class Post_Kinds_Plugin {
     60        /**
     61         * Plugin version, read from the plugin header on init.
     62         *
     63         * @since 2.4.0
     64         * @var string
     65         */
    5466        public static $version;
    5567
     68        /**
     69         * Sets the version and registers the kind taxonomy.
     70         *
     71         * @since 2.4.0
     72         */
    5673        public static function init() {
    5774                self::$version = get_file_data( __FILE__, array( 'Version' => 'Version' ) )['Version'];
    … …  
    6178        }
    6279
     80        /**
     81         * Outputs an admin notice when Parse This is not available at all.
     82         *
     83         * @since 3.1.0
     84         */
    6385        public static function parse_this_error() {
    64                 $class   = 'notice notice-error';
    65                 $message = __( 'Parse This is not installed. Please advise the developer', 'indieweb-post-kinds' );
    66                 printf( '<div class="%1$s"><p>%2$s</p></div>', esc_attr( $class ), esc_html( $message ) );
    67         }
    68 
     86                wp_admin_notice(
     87                        esc_html__( 'Parse This is not installed. Please advise the developer', 'indieweb-post-kinds' ),
     88                        array( 'type' => 'error' )
     89                );
     90        }
     91
     92        /**
     93         * Outputs an admin notice when another plugin loaded a copy of Parse This
     94         * older than 2.0.0.
     95         *
     96         * Post Kinds still works, because its bundled 2.0 API is loaded alongside the
     97         * older copy, but the older copy serves the parse-this REST route used by the
     98         * reply metabox.
     99         *
     100         * @since 4.0.0
     101         */
     102        public static function parse_this_outdated_notice() {
     103                if ( ! current_user_can( 'activate_plugins' ) ) {
     104                        return;
     105                }
     106                wp_admin_notice(
     107                        sprintf(
     108                                /* translators: %s: Name of the plugin, or path of the file, that loaded the older copy of Parse This. */
     109                                esc_html__( 'Post Kinds: %s is loading a copy of Parse This older than version 2.0. Post Kinds will keep working, but URL lookups in the post editor use that older copy. Updating that plugin, or installing the Parse This plugin (version 2.0 or later), resolves this.', 'indieweb-post-kinds' ),
     110                                '<strong>' . esc_html( self::parse_this_source() ) . '</strong>'
     111                        ),
     112                        array( 'type' => 'warning' )
     113                );
     114        }
     115
     116        /**
     117         * Returns whether the loaded copy of Parse This is version 2.0.0 or later.
     118         *
     119         * Copies older than 2.0.0 do not define PARSE_THIS_VERSION.
     120         *
     121         * @since 4.0.0
     122         *
     123         * @return bool
     124         */
     125        public static function parse_this_is_current() {
     126                return defined( 'PARSE_THIS_VERSION' ) && version_compare( PARSE_THIS_VERSION, '2.0.0', '>=' );
     127        }
     128
     129        /**
     130         * Returns a name for whatever loaded Parse This: the plugin's name, or the
     131         * file's path relative to the content directory if it isn't a plugin.
     132         *
     133         * @since 4.0.0
     134         *
     135         * @return string
     136         */
     137        public static function parse_this_source() {
     138                if ( ! function_exists( 'parse_this_loader' ) ) {
     139                        return '';
     140                }
     141                $file = wp_normalize_path( ( new ReflectionFunction( 'parse_this_loader' ) )->getFileName() );
     142                $dir  = trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) );
     143                if ( 0 === strpos( $file, $dir ) ) {
     144                        $slug = strtok( substr( $file, strlen( $dir ) ), '/' );
     145                        if ( ! function_exists( 'get_plugins' ) ) {
     146                                require_once ABSPATH . 'wp-admin/includes/plugin.php';
     147                        }
     148                        foreach ( get_plugins() as $basename => $data ) {
     149                                if ( 0 === strpos( $basename, $slug . '/' ) ) {
     150                                        return $data['Name'];
     151                                }
     152                        }
     153                        return $slug;
     154                }
     155                return str_replace( trailingslashit( wp_normalize_path( WP_CONTENT_DIR ) ), '', $file );
     156        }
     157
     158        /**
     159         * Whether to check for the block editor and warn about it.
     160         *
     161         * Not on ClassicPress, or when the Classic Editor plugin is active.
     162         *
     163         * @since 3.3.4
     164         *
     165         * @return bool
     166         */
    69167        public static function show_editor_error() {
    70                 // Do not show this error in ClassicPress
     168                // Do not show this error in ClassicPress.
    71169                if ( self::is_classicpress() ) {
    72170                        return false;
    73171                }
    74                 // Do not show if less than Version 5
    75                 if ( version_compare( get_bloginfo( 'version' ), '5.0' ) <= 0 ) {
    76                         return false;
    77                 }
    78172                if ( class_exists( 'Classic_Editor' ) ) {
    79173                        return false;
    … …  
    82176        }
    83177
     178        /**
     179         * Whether the site runs ClassicPress.
     180         *
     181         * @since 3.6.0
     182         *
     183         * @return bool
     184         */
    84185        public static function is_classicpress() {
    85186                return function_exists( 'classicpress_version' );
    86187        }
    87188
     189        /**
     190         * Outputs an admin notice on screens that use the block editor.
     191         *
     192         * Post Kinds works only with the Classic Editor. The notice is not shown
     193         * if the Classic Editor or Disable Gutenberg plugin is active.
     194         *
     195         * @since 3.2.6
     196         *
     197         * @return string|void An empty string if no notice is needed.
     198         */
    88199        public static function classic_editor_error() {
    89200                if ( ! self::post_uses_gutenberg() ) {
    … …  
    101212
    102213
     214        /**
     215         * Flushes the rewrite rules after Post Kinds is updated.
     216         *
     217         * @since 3.4.19
     218         *
     219         * @param WP_Upgrader $upgrade_object The upgrader.
     220         * @param array       $options        Details of the update.
     221         */
    103222        public static function upgrader_process_complete( $upgrade_object, $options ) {
    104223                $current_plugin_path_name = plugin_basename( __FILE__ );
    … …  
    112231        }
    113232
     233        /**
     234         * Creates the kind terms and flushes the rewrite rules on activation.
     235         *
     236         * @since 3.3.0
     237         */
    114238        public static function activate() {
    115239                Kind_Taxonomy::activate_kinds();
    … …  
    117241        }
    118242
     243        /**
     244         * Flushes the rewrite rules on deactivation.
     245         *
     246         * @since 3.3.0
     247         */
    119248        public static function deactivate() {
    120249                flush_rewrite_rules();
    121250        }
    122251
     252        /**
     253         * Loads the translations, global functions and Parse This, and adds the hooks.
     254         *
     255         * @since 2.5.2
     256         */
    123257        public static function plugins_loaded() {
    124258                $cls = get_called_class();
    125259                load_plugin_textdomain( 'indieweb-post-kinds', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
    126260
    127                 // Add WordPress Compatibility File for Functions Introduced Post 4.9.9.
     261                // Polyfills for functions newer than the plugin's minimums.
    128262                require_once plugin_dir_path( __FILE__ ) . 'includes/compat.php';
    129                 // Parse this Compat File.
    130                 require_once plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/compat-functions.php';
    131263
    132264                // Add Kind Global Functions.
    … …  
    136268                require_once plugin_dir_path( __FILE__ ) . '/includes/time-functions.php';
    137269
    138                 // Parse This
    139                 require_once plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/autoload.php';
    140                 if ( ! class_exists( 'REST_Parse_This' ) ) {
    141                         require_once plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/class-rest-parse-this.php';
    142                 }
    143                 require_once plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/functions.php';
     270                /*
     271                 * Parse This. The standalone plugin, if active, has already loaded at
     272                 * plugins_loaded priority 9 and defined parse_this_loader(). Otherwise
     273                 * load the bundled copy. Its loader sets up the classes, the functions
     274                 * and the REST route.
     275                 */
     276                $parse_this = plugin_dir_path( __FILE__ ) . 'lib/parse-this/parse-this.php';
     277                if ( ! function_exists( 'parse_this_loader' ) && file_exists( $parse_this ) ) {
     278                        require_once $parse_this;
     279                        parse_this_loader();
     280                }
     281
     282                if ( ! function_exists( 'parse_this_loader' ) ) {
     283                        add_action( 'admin_notices', array( $cls, 'parse_this_error' ) );
     284                } elseif ( ! self::parse_this_is_current() ) {
     285                        /*
     286                         * Another plugin loaded a copy older than 2.0.0. Load the bundled 2.0
     287                         * API alongside it: everything in 2.0 is in the ParseThis namespace,
     288                         * so it does not clash with the old copy's global names, and both
     289                         * versions only load their bundled Mf2\Parser and Masterminds\HTML5
     290                         * if those classes do not already exist.
     291                         */
     292                        $includes = plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/';
     293                        if ( file_exists( $includes . 'autoload.php' ) ) {
     294                                require_once $includes . 'autoload.php';
     295                                require_once $includes . 'functions.php';
     296                        }
     297                        add_action( 'admin_notices', array( $cls, 'parse_this_outdated_notice' ) );
     298                }
    144299                $class_load = array(
    145                         'Plugins', // Plugin Specific Customization
    146                         'Media_Metadata', // Media Metadata Enhancements
    147                         'Config', // Configuration Menu
    148                         'Metabox', // Metabox for Classic Editor
    149                         'View', // Kind Display Functionality
     300                        'Plugins', // Plugin Specific Customization.
     301                        'Media_Metadata', // Media Metadata Enhancements.
     302                        'Config', // Configuration Menu.
     303                        'Metabox', // Metabox for Classic Editor.
     304                        'View', // Kind Display Functionality.
     305                        'Upgrade', // One-time upgrade of stored data.
    150306                );
    151307
    152308                foreach ( $class_load as $load ) {
    153309                        add_action( 'init', array( 'Kind_' . $load, 'init' ) );
     310                }
     311
     312                if ( defined( 'WP_CLI' ) && WP_CLI ) {
     313                        WP_CLI::add_command( 'post-kinds upgrade', array( 'Kind_Upgrade', 'cli' ) );
    154314                }
    155315
    … …  
    162322                add_action( 'admin_enqueue_scripts', array( $cls, 'admin_style_load' ) );
    163323
    164                 // Load Privacy Declaration
     324                // Load Privacy Declaration.
    165325                add_action( 'admin_init', array( $cls, 'privacy_declaration' ) );
    166                 remove_all_actions( 'do_feed_rss2' );
    167                 remove_all_actions( 'do_feed_atom' );
    168                 add_action( 'do_feed_rss2', array( $cls, 'do_feed_rss2' ), 10, 1 );
    169                 add_action( 'do_feed_atom', array( $cls, 'do_feed_atom' ), 10, 1 );
    170 
    171                 // Register Widgets
     326
     327                // Register Widgets.
    172328                add_action(
    173329                        'widgets_init',
    … …  
    179335        }
    180336
    181         public static function do_feed_atom( $for_comments ) {
    182                 if ( $for_comments ) {
    183                         load_template( plugin_dir_path( __FILE__ ) . 'templates/feed-atom-comments.php' );
    184                 } else {
    185                         load_template( plugin_dir_path( __FILE__ ) . 'templates/feed-atom.php' );
    186                 }
    187         }
    188 
    189         public static function do_feed_rss2( $for_comments ) {
    190                 if ( $for_comments ) {
    191                         load_template( plugin_dir_path( __FILE__ ) . 'templates/feed-rss2-comments.php' );
    192                 } else {
    193                         load_template( plugin_dir_path( __FILE__ ) . 'templates/feed-rss2.php' );
    194                 }
    195         }
    196 
    197337        /**
    198338         * Adds link to Plugin Page for Options Page.
     339         *
     340         * @since 2.4.0
    199341         *
    200342         * @access public
    … …  
    217359        /**
    218360         * Loads the Stylesheet for the Plugin.
     361         *
     362         * @since 2.4.0
    219363         */
    220364        public static function style_load() {
    … …  
    224368        /**
    225369         * Loads the Admin Stylesheet for the Plugin.
     370         *
     371         * @since 2.4.0
    226372         */
    227373        public static function admin_style_load() {
    … …  
    229375        }
    230376
     377        /**
     378         * Adds suggested text to the privacy policy guide.
     379         *
     380         * @since 3.0.5
     381         */
    231382        public static function privacy_declaration() {
    232383                if ( function_exists( 'wp_add_privacy_policy_content' ) ) {
    … …  
    242393        }
    243394
     395        /**
     396         * Whether the current screen uses the block editor.
     397         *
     398         * Screens other than the post editor count as using it.
     399         *
     400         * @since 3.3.2
     401         *
     402         * @return bool
     403         */
    244404        public static function post_uses_gutenberg() {
    245405                $screen = get_current_screen();
  • indieweb-post-kinds/tags/4.0.0/js/clone-media-fragment.js

    r2110745 r3738687  
    11function cloneMediaFragment() {
    2   // Check that the fragment is a Media Fragment (starts with t=)
    3   if(window.location.hash && window.location.hash.match(/^#t=/)) {
    4     // Find any video and audio tags on the page
    5     document.querySelectorAll("video,audio").forEach(function(el){
    6       // Create a virtual element to use as a URI parser
    7       var parser = document.createElement('a');
    8       parser.href = el.currentSrc;
    9       // Replace the hash
    10       parser.hash = window.location.hash;
    11       // Set the src of the video/audio tag to the full URL
    12       el.src = parser.href;
    13     });
    14   }
     2        // Check that the fragment is a Media Fragment (starts with t=)
     3        if ( window.location.hash && window.location.hash.match( /^#t=/ ) ) {
     4                // Find any video and audio tags on the page
     5                document.querySelectorAll( 'video,audio' ).forEach( function( el ) {
     6                        // Create a virtual element to use as a URI parser
     7                        var parser = document.createElement( 'a' );
     8                        parser.href = el.currentSrc;
     9                        // Replace the hash
     10                        parser.hash = window.location.hash;
     11                        // Set the src of the video/audio tag to the full URL
     12                        el.src = parser.href;
     13                } );
     14        }
    1515}
    1616
    17 document.addEventListener("DOMContentLoaded", function() {
    18   cloneMediaFragment();
    19   // When the media is paused, update the fragment of the page
    20   document.querySelectorAll("video,audio").forEach(function(el) {
    21     el.addEventListener("pause", function(event) {
    22       // Update the media fragment to the current time
    23       // Use replaceState to avoid triggering the "hashchange" listener above
    24       history.replaceState(null, null, "#t=" + Math.round(event.target.currentTime));
    25     });
    26   });
    27 });
     17document.addEventListener( 'DOMContentLoaded', function() {
     18        cloneMediaFragment();
     19        // When the media is paused, update the fragment of the page
     20        document.querySelectorAll( 'video,audio' ).forEach( function( el ) {
     21                el.addEventListener( 'pause', function( event ) {
     22                        // Update the media fragment to the current time
     23                        // Use replaceState to avoid triggering the "hashchange" listener above
     24                        history.replaceState( null, null, '#t=' + Math.round( event.target.currentTime ) );
     25                } );
     26        } );
     27} );
    2828
    2929// If the user changes the hash manually, clone the fragment to the media URLs
    30 window.addEventListener("hashchange", cloneMediaFragment);
     30window.addEventListener( 'hashchange', cloneMediaFragment );
  • indieweb-post-kinds/tags/4.0.0/js/kind.js

    r3001401 r3738687  
     1/* global PKAPI, moment, wp */
     2/* eslint-disable no-alert -- The metabox uses alert() and confirm() for feedback by design. */
    13jQuery( document ).ready( function( $ ) {
     4        var KindWindow;
     5
    26        changeSettings();
    37
    4 
    5 function clearPostProperties() {
    6         var fieldIds = [
    7                 'cite_url',
    8                 'cite_name',
    9                 'cite_summary',
    10                 'cite_tags',
    11                 'cite_media',
    12                 'cite_author_name',
    13                 'cite_author_url',
    14                 'cite_author_photo',
    15                 'cite_featured',
    16                 'cite_publication',
    17                 'mf2_rsvp',
    18                 'mf2_rating',
    19                 'mf2_start_time',
    20                 'mf2_start_date',
    21                 'mf2_end_time',
    22                 'mf2_end_date',
    23                 'cite_published_time',
    24                 'cite_published_date',
    25                 'cite_updated_time',
    26                 'cite_updated_date',
    27                 'duration_years',
    28                 'duration_months',
    29                 'duration_days',
    30                 'duration_hours',
    31                 'duration_minutes',
    32                 'duration_seconds'
    33         ];
     8        function clearPostProperties() {
     9                var fieldIds = [
     10                        'cite_url',
     11                        'cite_name',
     12                        'cite_summary',
     13                        'cite_tags',
     14                        'cite_media',
     15                        'cite_author_name',
     16                        'cite_author_url',
     17                        'cite_author_photo',
     18                        'cite_featured',
     19                        'cite_publication',
     20                        'mf2_rsvp',
     21                        'mf2_rating',
     22                        'mf2_start_time',
     23                        'mf2_start_date',
     24                        'mf2_end_time',
     25                        'mf2_end_date',
     26                        'cite_published_time',
     27                        'cite_published_date',
     28                        'cite_updated_time',
     29                        'cite_updated_date',
     30                        'duration_years',
     31                        'duration_months',
     32                        'duration_days',
     33                        'duration_hours',
     34                        'duration_minutes',
     35                        'duration_seconds',
     36                ];
    3437                if ( ! confirm( PKAPI.clear_message ) ) {
    3538                        return;
    … …  
    3740                $.each( fieldIds, function( count, val ) {
    3841                        document.getElementById( val ).value = '';
    39                 });
     42                } );
    4043                $( '#kind-media-container' ).addClass( 'hidden' );
    4144                $( '#kind-media-container' ).children( 'img' ).hide();
    4245                $( '#add-kind-media' ).show();
    43 
    44 }
    45 
    46 function addhttp( url ) {
    47         if ( ! /^(?:f|ht)tps?\:\/\//.test( url ) ) {
    48                 url = 'http://' + url;
    49         }
    50         return url;
    51 }
    52 
    53 function showLoadingSpinner() {
    54         $( '#replybox-meta' ).addClass( 'is-loading' );
    55 }
    56 
    57 function hideLoadingSpinner() {
    58         $( '#replybox-meta' ).removeClass( 'is-loading' );
    59 }
    60 
    61 //function used to validate website URL
    62 function checkUrl( url ) {
    63 
    64     //regular expression for URL
    65     var pattern = /^(http|https)?:\/\/[a-zA-Z0-9-\.]+\.[a-z]{2,4}/;
    66 
    67     if ( pattern.test( url ) ) {
    68         return true;
    69     } else {
    70         return false;
    71     }
    72 }
    73 
    74 function getLinkPreview() {
    75         if ( '' === $( '#cite_url' ).val() ) {
    76                 return;
    77         }
    78         $.ajax({
    79                 type: 'GET',
    80 
    81                 // Here we supply the endpoint url, as opposed to the action in the data object with the admin-ajax method
    82                 url: PKAPI.api_url + 'parse/',
    83                 beforeSend: function( xhr ) {
    84 
    85                 // Here we set a header 'X-WP-Nonce' with the nonce as opposed to the nonce in the data object with admin-ajax
    86                 xhr.setRequestHeader( 'X-WP-Nonce', PKAPI.api_nonce );
    87                 },
    88                 data: {
    89                         url: $( '#cite_url' ).val(),
    90                         kind: $( 'input[name=\'tax_input[kind]\']:checked' ).val(),
    91                         follow: true
    92                 },
    93                 success: function( response ) {
    94                         var published;
    95                         var updated;
    96                         if ( 'undefined' === typeof response ) {
    97                                 alert( 'Error: Unable to Retrieve' );
     46        }
     47
     48        function showLoadingSpinner() {
     49                $( '#replybox-meta' ).addClass( 'is-loading' );
     50        }
     51
     52        function hideLoadingSpinner() {
     53                $( '#replybox-meta' ).removeClass( 'is-loading' );
     54        }
     55
     56        //function used to validate website URL
     57        function checkUrl( url ) {
     58                //regular expression for URL
     59                var pattern = /^(http|https)?:\/\/[a-zA-Z0-9-\.]+\.[a-z]{2,4}/;
     60
     61                return pattern.test( url );
     62        }
     63
     64        // Fills the Author tab from one author or a list. Each field holds one value
     65        // per author, separated by semicolons, so the values stay paired by position.
     66        function fillAuthors( authors ) {
     67                var fields = { name: [], url: [], photo: [] };
     68                if ( ! Array.isArray( authors ) ) {
     69                        authors = [ authors ];
     70                }
     71                $.each( authors, function( index, author ) {
     72                        if ( 'string' === typeof author ) {
     73                                author = checkUrl( author ) ? { url: author } : { name: author };
     74                        }
     75                        if ( ! author || 'object' !== typeof author ) {
    9876                                return;
    9977                        }
    100                         if ( 'message' in response ) {
    101                                 alert( response.message );
     78                        $.each( fields, function( key, values ) {
     79                                var value = author[ key ];
     80                                if ( Array.isArray( value ) ) {
     81                                        value = value.join( '; ' );
     82                                } else if ( value && 'object' === typeof value ) {
     83                                        value = value.value;
     84                                }
     85                                values.push( 'string' === typeof value ? value : '' );
     86                        } );
     87                } );
     88                $.each( fields, function( key, values ) {
     89                        $( '#cite_author_' + key ).val( values.join( '' ) ? values.join( '; ' ) : '' );
     90                } );
     91        }
     92
     93        function getLinkPreview() {
     94                if ( '' === $( '#cite_url' ).val() ) {
     95                        return;
     96                }
     97                $.ajax( {
     98                        type: 'GET',
     99
     100                        // Here we supply the endpoint url, as opposed to the action in the data object with the admin-ajax method
     101                        url: PKAPI.api_url + 'parse/',
     102                        beforeSend: function( xhr ) {
     103                                // Here we set a header 'X-WP-Nonce' with the nonce as opposed to the nonce in the data object with admin-ajax
     104                                xhr.setRequestHeader( 'X-WP-Nonce', PKAPI.api_nonce );
     105                        },
     106                        data: {
     107                                url: $( '#cite_url' ).val(),
     108                                follow: true,
     109                        },
     110                        success: function( response ) {
     111                                var published, updated, duration;
     112                                if ( 'undefined' === typeof response ) {
     113                                        alert( PKAPI.error_message );
     114                                        return;
     115                                }
     116                                if ( 'message' in response ) {
     117                                        alert( response.message );
     118                                        return;
     119                                }
     120                                if ( 'name' in response ) {
     121                                        $( '#cite_name' ).val( response.name );
     122                                }
     123                                if ( 'published' in response ) {
     124                                        published = moment.parseZone( response.published );
     125                                        $( '#cite_published_date' ).val( published.format( 'YYYY-MM-DD' ) );
     126                                        $( '#cite_published_time' ).val( published.format( 'HH:mm:ss' ) );
     127                                        $( '#cite_published_offset' ).val( published.format( 'Z' ) );
     128                                }
     129                                if ( 'updated' in response ) {
     130                                        updated = moment.parseZone( response.updated );
     131                                        $( '#cite_updated_date' ).val( updated.format( 'YYYY-MM-DD' ) );
     132                                        $( '#cite_updated_time' ).val( updated.format( 'HH:mm:ss' ) );
     133                                        $( '#cite_updated_offset' ).val( updated.format( 'Z' ) );
     134                                }
     135                                if ( 'duration' in response ) {
     136                                        duration = moment.duration( response.duration );
     137                                        $( '#duration_years' ).val( duration.years() );
     138                                        $( '#duration_months' ).val( duration.months() );
     139                                        $( '#duration_days' ).val( duration.days() );
     140                                        $( '#duration_hours' ).val( duration.hours() );
     141                                        $( '#duration_minutes' ).val( duration.minutes() );
     142                                        $( '#duration_seconds' ).val( duration.seconds() );
     143                                }
     144
     145                                if ( 'summary' in response ) {
     146                                        $( '#cite_summary' ).val( response.summary );
     147                                }
     148                                if ( 'featured' in response ) {
     149                                        $( '#cite_featured' ).val( response.featured );
     150                                }
     151                                if ( 'author' in response ) {
     152                                        fillAuthors( response.author );
     153                                }
     154                                if ( 'publication' in response && ( 'string' !== typeof response.publication ) ) {
     155                                        if ( 'name' in response.publication ) {
     156                                                $( '#cite_publication' ).val( response.publication.name );
     157                                        }
     158                                } else {
     159                                        $( '#cite_publication' ).val( response.publication );
     160                                }
     161
     162                                if ( 'category' in response ) {
     163                                        if ( 'object' === typeof response.category ) {
     164                                                $( '#cite_tags' ).val( response.category.join( ';' ) );
     165                                        }
     166                                }
     167                                alert( PKAPI.success_message );
     168                        },
     169                        error: function( jqXHR ) {
     170                                if ( jqXHR.responseJSON && jqXHR.responseJSON.message ) {
     171                                        alert( jqXHR.responseJSON.message );
     172                                } else {
     173                                        alert( PKAPI.error_message );
     174                                }
     175                        },
     176                        complete: hideLoadingSpinner,
     177                } );
     178        }
     179
     180        function changeSettings() {
     181                var kind = $( 'input[name=\'tax_input[kind]\']:checked' ).val();
     182                switch ( kind ) {
     183                        case 'note':
     184                                hideTitle();
     185                                hideReply();
     186                                hideRSVP();
     187                                hideRating();
     188                                hideTime();
     189                                hideMedia();
     190                                break;
     191                        case 'article':
     192                                showTitle();
     193                                hideReply();
     194                                hideRSVP();
     195                                hideRating();
     196                                hideTime();
     197                                hideMedia();
     198                                break;
     199                        case 'issue':
     200                                showTitle();
     201                                showReply();
     202                                hideRSVP();
     203                                hideRating();
     204                                hideTime();
     205                                hideMedia();
     206                                break;
     207                        case 'listen':
     208                        case 'jam':
     209                        case 'watch':
     210                        case 'read':
     211                        case 'play':
     212                                hideTitle();
     213                                showReply();
     214                                hideRSVP();
     215                                showRating();
     216                                showTime();
     217                                hideMedia();
     218                                break;
     219                        case 'photo':
     220                        case 'video':
     221                        case 'audio':
     222                                hideTitle();
     223                                showReply();
     224                                hideRSVP();
     225                                hideRating();
     226                                showTime();
     227                                showMedia();
     228                                break;
     229                        case 'rsvp':
     230                                showReply();
     231                                hideTitle();
     232                                hideTime();
     233                                showRSVP();
     234                                hideRating();
     235                                hideMedia();
     236                                break;
     237                        case 'review':
     238                        case 'drink':
     239                        case 'eat':
     240                                showReply();
     241                                hideTitle();
     242                                hideTime();
     243                                hideRSVP();
     244                                showRating();
     245                                hideMedia();
     246                                break;
     247                        default:
     248                                showReply();
     249                                hideTime();
     250                                hideTitle();
     251                                hideRSVP();
     252                                hideRating();
     253                                hideMedia();
     254                }
     255        }
     256
     257        function showReply() {
     258                $( '#replybox-meta' ).removeClass( 'hidden' );
     259        }
     260
     261        function hideReply() {
     262                $( '#replybox-meta' ).addClass( 'hidden' );
     263        }
     264
     265        function showMedia() {
     266                $( '#add-kind-media' ).removeClass( 'hidden' );
     267        }
     268
     269        function hideMedia() {
     270                $( '#add-kind-media' ).addClass( 'hidden' );
     271        }
     272
     273        function showTitle() {
     274                var titlediv = $( '#titlediv' ).detach();
     275                titlediv.prependTo( '#post-body-content' );
     276        }
     277
     278        function hideTitle() {
     279                var titlediv = $( '#titlediv' ).detach();
     280                titlediv.insertAfter( '#postdivrich' );
     281        }
     282
     283        function showRSVP() {
     284                $( '#rsvp-option' ).removeClass( 'hide-if-js' );
     285        }
     286
     287        function hideRSVP() {
     288                $( '#rsvp-option' ).addClass( 'hide-if-js' );
     289        }
     290
     291        function showRating() {
     292                $( '#rating-option' ).removeClass( 'hide-if-js' );
     293        }
     294
     295        function hideRating() {
     296                $( '#rating-option' ).addClass( 'hide-if-js' );
     297        }
     298
     299        function showTime() {
     300                $( '#kind-time' ).removeClass( 'hide-if-js' );
     301        }
     302
     303        function hideTime() {
     304                $( '#kind-time' ).addClass( 'hide-if-js' );
     305        }
     306
     307        function handleKindMediaWindow() {
     308                'use strict';
     309
     310                var json;
     311
     312                /**
     313                 * If an instance of KindWindow already exists, then we can open it
     314                 * rather than creating a new instance.
     315                 */
     316                if ( undefined !== KindWindow ) {
     317                        KindWindow.open();
     318                        return;
     319                }
     320
     321                KindWindow = wp.media.frames.KindWindow = wp.media( {
     322                        title: PKAPI.media_title,
     323                        button: {
     324                                text: PKAPI.media_button,
     325                        },
     326                        multiple: false,
     327                } );
     328
     329                KindWindow.on( 'select', function() {
     330                        json = KindWindow.state().get( 'selection' ).first().toJSON();
     331                        if ( ! json.url ) {
    102332                                return;
    103333                        }
    104                         if ( 'name' in response ) {
    105                                 $( '#cite_name' ).val( response.name );
    106                         }
    107                         if ( 'published' in response ) {
    108                                 var published = moment.parseZone( response.published );
    109                                 $( '#cite_published_date' ).val( published.format( 'YYYY-MM-DD' ) ) ;
    110                                 $( '#cite_published_time' ).val( published.format( 'HH:mm:ss' ) ) ;
    111                                 $( '#cite_published_offset' ).val( published.format( 'Z' ) );
    112                         }
    113                         if ( 'updated' in response ) {
    114                                 var updated = moment.parseZone( response.updated );
    115                                 $( '#cite_updated_date' ).val( updated.format( 'YYYY-MM-DD' ) ) ;
    116                                 $( '#cite_updated_time' ).val( updated.format( 'HH:mm:ss' ) ) ;
    117                                 $( '#cite_updated_offset' ).val( updated.format( 'Z' ) );
    118                         }
    119                         if ( 'duration' in response ) {
    120                                 var duration = moment.duration( response.duration);
    121                                 $( '#duration_years' ).val( duration.years() );
    122                                 $( '#duration_months' ).val( duration.months() );
    123                                 $( '#duration_days' ).val( duration.days() );
    124                                 $( '#duration_hours' ).val( duration.hours() );
    125                                 $( '#duration_minutes' ).val( duration.minutes() );
    126                                 $( '#duration_seconds' ).val( duration.seconds() );
    127                         }
    128 
    129                         if ( 'summary' in response ) {
    130                                 $( '#cite_summary' ).val( response.summary ) ;
    131                         }
    132                         if ( 'featured' in response ) {
    133                                 $( '#cite_featured' ).val( response.featured ) ;
    134                         }
    135                         if ( ( 'author' in response ) && ( 'string' != typeof response.author ) ) {
    136                                 if ( 'name' in response.author ) {
    137                                         if ( 'string' === typeof response.author.name ) {
    138                                                 $( '#cite_author_name' ).val( response.author.name );
    139                                         } else {
    140                                                 $( '#cite_author_name' ).val( response.author.name.join( ';' ) ) ;
    141                                         }
    142                                 }
    143                                 if ( 'photo' in response.author ) {
    144                                         if ( 'string' === typeof response.author.name ) {
    145                                                 $( '#cite_author_photo' ).val( response.author.photo );
    146                                         } else {
    147                                                 $( '#cite_author_photo' ).val( response.author.photo.join( ';' ) ) ;
    148                                         }
    149                                 }
    150                                 if ( 'url' in response.author ) {
    151                                         if ( 'string' === typeof response.author.url ) {
    152                                                 $( '#cite_author_url' ).val( response.author.url );
    153                                         } else {
    154                                                 $( '#cite_author_url' ).val( response.author.url.join( ';' ) ) ;
    155                                         }
    156                                 }
    157                         }
    158                         if ( 'publication' in response && ( 'string' != typeof response.publication ) ) {
    159                                 if ( 'name' in response.publication ) {
    160                                         $( '#cite_publication' ).val( response.publication.name );
    161                                 }
     334                        $( '#cite_name' ).val( json.title );
     335                        $( '#cite_url' ).val( json.url );
     336                        $( '#cite_media' ).val( json.id );
     337                        $( '#cite_summary' ).val( json.description );
     338                        $( '#kind-media-container' )
     339                                .children( 'img' )
     340                                .attr( 'src', json.url )
     341                                .attr( 'alt', json.caption )
     342                                .attr( 'title', json.title )
     343                                .show()
     344                                .parent()
     345                                .removeClass( 'hidden' );
     346
     347                        $( '#add-kind-media' ).hide();
     348                } );
     349                KindWindow.open();
     350        }
     351
     352        jQuery( document )
     353                .on( 'change', '#taxonomy-kind', function( event ) {
     354                        changeSettings();
     355                        event.preventDefault();
     356                } )
     357                .on( 'blur', '#cite_url', function( event ) {
     358                        if ( '' !== $( '#cite_url' ).val() ) {
     359                                if ( false === checkUrl( $( '#cite_url' ).val() ) ) {
     360                                        alert( PKAPI.invalid_url );
     361                                } else if ( '' === $( '#cite_name' ).val() ) {
     362                                        showLoadingSpinner();
     363                                        getLinkPreview();
     364                                }
     365                                event.preventDefault();
     366                        }
     367                } )
     368                .on( 'click', '.clear-kindmeta-button', function( event ) {
     369                        clearPostProperties();
     370                        event.preventDefault();
     371                } )
     372                .on( 'click', 'a.show-kind-details', function( event ) {
     373                        if ( $( '#kind-details' ).is( ':hidden' ) ) {
     374                                $( '#kind-author' ).slideUp( 'fast' ).siblings( 'a.show-kind-author' );
     375                                $( '#kind-details' ).slideDown( 'fast' ).siblings( 'a.hide-kind-details' ).show().focus();
    162376                        } else {
    163                                 $( '#cite_publication' ).val( response.publication ) ;
    164                         }
    165 
    166                         if ( 'category' in response ) {
    167                                 if ( 'object' === typeof response.category ) {
    168                                         $( '#cite_tags' ).val( response.category.join( ';' ) );
    169                                 }
    170                         }
    171                 alert( PKAPI.success_message );
    172                 console.log( response );
    173                 },
    174                 fail: function( response ) {
    175                         console.log( response );
    176                         alert( response.message );
    177                 },
    178                 error: function( jqXHR, textStatus, errorThrown ) {
    179                         alert( jqXHR.responseJSON.message );
    180                         console.log( jqXHR );
    181                 },
    182                 always: hideLoadingSpinner()
    183         });
    184 }
    185 
    186 function changeSettings() {
    187         kind = $( 'input[name=\'tax_input[kind]\']:checked' ).val();
    188         switch ( kind ) {
    189                 case 'note':
    190                         hideTitle();
    191                         hideReply();
    192                         hideRSVP();
    193                         hideRating();
    194                         hideTime();
    195                         hideMedia();
    196                         break;
    197                 case 'article':
    198                         showTitle();
    199                         hideReply();
    200                         hideRSVP();
    201                         hideRating();
    202                         hideTime();
    203                         hideMedia();
    204                         break;
    205                 case 'issue':
    206                         showTitle();
    207                         showReply();
    208                         hideRSVP();
    209                         hideRating();
    210                         hideTime();
    211                         hideMedia();
    212                         break;
    213                 case 'listen':
    214                 case 'jam':
    215                 case 'watch':
    216                 case 'read':
    217                 case 'play':
    218                         hideTitle();
    219                         showReply();
    220                         hideRSVP();
    221                         showRating();
    222                         showTime();
    223                         hideMedia();
    224                         break;
    225                 case 'photo':
    226                 case 'video':
    227                 case 'audio':
    228                         hideTitle();
    229                         showReply();
    230                         hideRSVP();
    231                         hideRating();
    232                         showTime();
    233                         showMedia();
    234                         break;
    235                 case 'rsvp':
    236                         showReply();
    237                         hideTitle();
    238                         hideTime();
    239                         showRSVP();
    240                         hideRating();
    241                         hideMedia();
    242                         break;
    243                 case 'review':
    244                 case 'drink':
    245                 case 'eat':
    246                         showReply();
    247                         hideTitle();
    248                         hideTime();
    249                         hideRSVP();
    250                         showRating();
    251                         hideMedia();
    252                         break;
    253                 default:
    254                         showReply();
    255                         hideTime();
    256                         hideTitle();
    257                         hideRSVP();
    258                         hideRating();
    259                         hideMedia();
    260         }
    261 }
    262 
    263 function showReply() {
    264         $( '#replybox-meta' ).removeClass( 'hidden' );
    265 }
    266 
    267 function hideReply() {
    268         $( '#replybox-meta' ).addClass( 'hidden' );
    269 }
    270 
    271 function showMedia() {
    272         $( '#add-kind-media' ).removeClass( 'hidden' );
    273 }
    274 
    275 function hideMedia() {
    276         $( '#add-kind-media' ).addClass( 'hidden' );
    277 }
    278 
    279 
    280 function showTitle() {
    281         var titlediv = $( '#titlediv' ).detach();
    282         titlediv.prependTo( '#post-body-content' );
    283 }
    284 
    285 function hideTitle() {
    286         var titlediv = $( '#titlediv' ).detach();
    287         titlediv.insertAfter( '#postdivrich' );
    288 }
    289 
    290 function showRSVP() {
    291         $( '#rsvp-option' ).removeClass( 'hide-if-js' );
    292 }
    293 
    294 function hideRSVP() {
    295         $( '#rsvp-option' ).addClass( 'hide-if-js' );
    296 }
    297 
    298 function showRating() {
    299         $( '#rating-option' ).removeClass( 'hide-if-js' );
    300 }
    301 
    302 function hideRating() {
    303         $( '#rating-option' ).addClass( 'hide-if-js' );
    304 }
    305 
    306 function showTime() {
    307         $( '#kind-time' ).removeClass( 'hide-if-js' );
    308 }
    309 
    310 function hideTime() {
    311         $( '#kind-time' ).addClass( 'hide-if-js' );
    312 }
    313 
    314 function handleKindMediaWindow() {
    315         'use strict';
    316 
    317         var KindWindow, ImageData, json;
    318 
    319         /**
    320          * If an instance of KindWindow already exists, then we can open it
    321          * rather than creating a new instance.
    322          */
    323         if ( undefined !== KindWindow ) {
    324                 KindWindow.open();
    325                 return;
    326         }
    327 
    328         KindWindow = wp.media.frames.KindWindow = wp.media({
    329                 title: 'Attach',
    330                 button: {
    331                         text: 'Use this media'
    332                 },
    333                 multiple: false
    334         });
    335 
    336         KindWindow.on( 'select', function() {
    337                 json = KindWindow.state().get( 'selection' ).first().toJSON();
    338                 console.log( json );
    339                 if ( 0 > $.trim( json.url.length ) ) {
    340                         return;
    341                 }
    342                 $( '#cite_name' ).val( json.title );
    343                 $( '#cite_url' ).val( json.url );
    344                 $( '#cite_media' ).val( json.id );
    345                 $( '#cite_summary' ).val( json.description );
    346                 $( '#kind-media-container' )
    347                         .children( 'img' )
    348                         .attr( 'src', json.url )
    349                         .attr( 'alt', json.caption )
    350                         .attr( 'title', json.title )
    351                         .show()
    352                         .parent()
    353                         .removeClass( 'hidden' );
    354 
    355                 $( '#add-kind-media' ).hide();
    356         });
    357         KindWindow.open();
    358 
    359 }
    360 
    361 jQuery( document )
    362         .on( 'change', '#taxonomy-kind', function( event ) {
    363                 changeSettings();
    364                 event.preventDefault();
    365         })
    366         .on( 'blur', '#cite_url', function( event ) {
    367                 if ( '' !== $( '#cite_url' ).val() ) {
    368                         if ( false == checkUrl( $( '#cite_url' ).val() ) ) {
    369                                 alert( 'Invalid URL' );
    370                         } else if ( '' === $( '#cite_name' ).val() ) {
    371                                 showLoadingSpinner();
    372                                 getLinkPreview();
    373                         }
    374                         event.preventDefault();
    375                 }
    376         })
    377         .on( 'click', '.clear-kindmeta-button', function( event ) {
    378                 clearPostProperties();
    379                 event.preventDefault();
    380         })
    381         .on( 'click', 'a.show-kind-details', function( event ) {
    382                 if ( $( '#kind-details' ).is( ':hidden' ) ) {
    383                         $( '#kind-author' ).slideUp( 'fast' ).siblings( 'a.show-kind-author' );
    384                         $( '#kind-details' ).slideDown( 'fast' ).siblings( 'a.hide-kind-details' ).show().focus();
    385                 } else {
    386                         $( '#kind-details' ).slideUp( 'fast' ).siblings( 'a.show-kind-details' ).focus();
    387                 }
    388                 event.preventDefault();
    389         })
    390         .on( 'click', 'a.show-kind-author-details', function( event ) {
    391                 if ( $( '#kind-author' ).is( ':hidden' ) ) {
    392                         $( '#kind-details' ).slideUp( 'fast' ).siblings( 'a.show-kind-details' );
    393                         $( '#kind-author' ).slideDown( 'fast' ).siblings( 'a.hide-kind-author' ).show().focus();
    394                 } else {
    395                         $( '#kind-author' ).slideUp( 'fast' ).siblings( 'a.show-kind-author' ).focus();
    396                 }
    397                 event.preventDefault();
    398         })
    399         .on( 'click', '#add-kind-media', function( event ) {
    400                 event.preventDefault();
    401                 handleKindMediaWindow();
    402         });
    403 });
     377                                $( '#kind-details' ).slideUp( 'fast' ).siblings( 'a.show-kind-details' ).focus();
     378                        }
     379                        event.preventDefault();
     380                } )
     381                .on( 'click', 'a.show-kind-author-details', function( event ) {
     382                        if ( $( '#kind-author' ).is( ':hidden' ) ) {
     383                                $( '#kind-details' ).slideUp( 'fast' ).siblings( 'a.show-kind-details' );
     384                                $( '#kind-author' ).slideDown( 'fast' ).siblings( 'a.hide-kind-author' ).show().focus();
     385                        } else {
     386                                $( '#kind-author' ).slideUp( 'fast' ).siblings( 'a.show-kind-author' ).focus();
     387                        }
     388                        event.preventDefault();
     389                } )
     390                .on( 'click', '#add-kind-media', function( event ) {
     391                        event.preventDefault();
     392                        handleKindMediaWindow();
     393                } );
     394} );
  • indieweb-post-kinds/tags/4.0.0/languages/indieweb-post-kinds.pot

    r3067251 r3738687  
    1 # Copyright (C) 2024 David Shanske
    2 # This file is distributed under the same license as the Post Kinds plugin.
     1# Copyright (C) 2026 David Shanske
     2# This file is distributed under the GPLv2 or later.
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Post Kinds 3.7.3\n"
     5"Project-Id-Version: Post Kinds 4.0.0\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/indieweb-post-kinds\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2024-04-09T03:10:32+00:00\n"
     12"POT-Creation-Date: 2026-10-10T20:26:16+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    14 "X-Generator: WP-CLI 2.10.0\n"
     14"X-Generator: WP-CLI 2.12.0\n"
    1515"X-Domain: indieweb-post-kinds\n"
    1616
    1717#. Plugin Name of the plugin
    18 #: indieweb-post-kinds.php
    19 #: includes/class-kind-config.php:251
    20 #: includes/class-kind-config.php:252
    21 #: includes/class-kind-config.php:258
     18#: indieweb-post-kinds.php:7
     19#: includes/class-kind-config.php:433
     20#: includes/class-kind-config.php:434
     21#: includes/class-kind-config.php:440
    2222msgid "Post Kinds"
    2323msgstr ""
    2424
    2525#. Plugin URI of the plugin
    26 #: indieweb-post-kinds.php
     26#: indieweb-post-kinds.php:8
    2727msgid "https://wordpress.org/plugins/indieweb-post-kinds/"
    2828msgstr ""
    2929
    3030#. Description of the plugin
    31 #: indieweb-post-kinds.php
     31#: indieweb-post-kinds.php:9
    3232msgid "Ever want to reply to someone else's post with a post on your own site? Or to \"like\" someone else's post, but with your own site?"
    3333msgstr ""
    3434
    3535#. Author of the plugin
    36 #: indieweb-post-kinds.php
     36#: indieweb-post-kinds.php:15
    3737msgid "David Shanske"
    3838msgstr ""
    3939
    4040#. Author URI of the plugin
    41 #: indieweb-post-kinds.php
     41#: indieweb-post-kinds.php:16
    4242msgid "https://david.shanske.com"
    4343msgstr ""
    4444
    45 #: includes/class-kind-config.php:96
     45#: includes/class-kind-config.php:224
     46msgid "The allowed HTML must be a valid JSON object. The previous value has been kept."
     47msgstr ""
     48
     49#: includes/class-kind-config.php:261
     50msgid "Kind base"
     51msgstr ""
     52
     53#: includes/class-kind-config.php:269
    4654msgid "Content Options"
    4755msgstr ""
    4856
    49 #: includes/class-kind-config.php:105
     57#: includes/class-kind-config.php:278
    5058msgid "Select All Kinds You Wish to Use"
    5159msgstr ""
    5260
    53 #: includes/class-kind-config.php:112
     61#: includes/class-kind-config.php:285
    5462msgid "Default Kind for New Posts"
    5563msgstr ""
    5664
    57 #: includes/class-kind-config.php:120
     65#: includes/class-kind-config.php:293
    5866msgid "Embed Sites into your Response"
    5967msgstr ""
    6068
    61 #: includes/class-kind-config.php:131
     69#: includes/class-kind-config.php:304
    6270msgid "Kinds to Show in Main Feed. Selecting None will Disable this Filter"
    6371msgstr ""
    6472
    65 #: includes/class-kind-config.php:142
     73#: includes/class-kind-config.php:315
    6674msgid "Move Response to Bottom of Content"
    6775msgstr ""
    6876
    69 #: includes/class-kind-config.php:153
     77#: includes/class-kind-config.php:326
    7078msgid "Display Before Kind"
    7179msgstr ""
    7280
    73 #: includes/class-kind-config.php:161
     81#: includes/class-kind-config.php:334
    7482msgid "Show Icon"
    7583msgstr ""
    7684
    77 #: includes/class-kind-config.php:162
     85#: includes/class-kind-config.php:335
    7886msgid "Show Text"
    7987msgstr ""
    8088
    81 #: includes/class-kind-config.php:163
     89#: includes/class-kind-config.php:336
    8290msgid "Show Icon and Text"
    8391msgstr ""
    8492
    85 #: includes/class-kind-config.php:164
     93#: includes/class-kind-config.php:337
    8694msgid "Display Nothing"
    8795msgstr ""
    8896
    89 #: includes/class-kind-config.php:172
     97#: includes/class-kind-config.php:345
    9098msgid "Response Content Allowed Html Elements"
    9199msgstr ""
    92100
    93 #: includes/class-kind-config.php:183
     101#: includes/class-kind-config.php:356
    94102msgid "Automatically add the Kind to the Title"
    95103msgstr ""
    96104
    97 #: includes/class-kind-config.php:391
     105#: includes/class-kind-config.php:593
    98106msgid "Indieweb Post Kinds"
    99107msgstr ""
    100108
    101 #: includes/class-kind-config.php:392
     109#: includes/class-kind-config.php:594
    102110msgid "Adds support for responding and interacting with other sites."
    103111msgstr ""
    104112
    105 #: includes/class-kind-config.php:415
     113#: includes/class-kind-config.php:619
    106114msgid "Post Properties"
    107115msgstr ""
    108116
    109 #: includes/class-kind-config.php:416
     117#: includes/class-kind-config.php:620
    110118msgid ""
    111119"\n"
    … …  
    139147msgstr ""
    140148
    141 #: includes/class-kind-fields.php:27
     149#: includes/class-kind-fields.php:39
    142150msgid "(GMT-11:00) Niue"
    143151msgstr ""
    144152
    145 #: includes/class-kind-fields.php:28
     153#: includes/class-kind-fields.php:40
    146154msgid "(GMT-11:00) Pago Pago"
    147155msgstr ""
    148156
    149 #: includes/class-kind-fields.php:29
     157#: includes/class-kind-fields.php:41
    150158msgid "(GMT-10:00) Hawaii Time"
    151159msgstr ""
    152160
    153 #: includes/class-kind-fields.php:30
     161#: includes/class-kind-fields.php:42
    154162msgid "(GMT-10:00) Rarotonga"
    155163msgstr ""
    156164
    157 #: includes/class-kind-fields.php:31
     165#: includes/class-kind-fields.php:43
    158166msgid "(GMT-10:00) Tahiti"
    159167msgstr ""
    160168
    161 #: includes/class-kind-fields.php:32
     169#: includes/class-kind-fields.php:44
    162170msgid "(GMT-09:30) Marquesas"
    163171msgstr ""
    164172
    165 #: includes/class-kind-fields.php:33
     173#: includes/class-kind-fields.php:45
    166174msgid "(GMT-09:00) Alaska Time"
    167175msgstr ""
    168176
    169 #: includes/class-kind-fields.php:34
     177#: includes/class-kind-fields.php:46
    170178msgid "(GMT-09:00) Gambier"
    171179msgstr ""
    172180
    173 #: includes/class-kind-fields.php:35
     181#: includes/class-kind-fields.php:47
    174182msgid "(GMT-08:00) Pacific Time"
    175183msgstr ""
    176184
    177 #: includes/class-kind-fields.php:36
     185#: includes/class-kind-fields.php:48
    178186msgid "(GMT-08:00) Pacific Time - Tijuana"
    179187msgstr ""
    180188
    181 #: includes/class-kind-fields.php:37
     189#: includes/class-kind-fields.php:49
    182190msgid "(GMT-08:00) Pacific Time - Vancouver"
    183191msgstr ""
    184192
    185 #: includes/class-kind-fields.php:38
     193#: includes/class-kind-fields.php:50
    186194msgid "(GMT-08:00) Pacific Time - Whitehorse"
    187195msgstr ""
    188196
    189 #: includes/class-kind-fields.php:39
     197#: includes/class-kind-fields.php:51
    190198msgid "(GMT-08:00) Pitcairn"
    191199msgstr ""
    192200
    193 #: includes/class-kind-fields.php:40
     201#: includes/class-kind-fields.php:52
    194202msgid "(GMT-07:00) Mountain Time - Dawson Creek"
    195203msgstr ""
    196204
    197 #: includes/class-kind-fields.php:41
     205#: includes/class-kind-fields.php:53
    198206msgid "(GMT-07:00) Mountain Time"
    199207msgstr ""
    200208
    201 #: includes/class-kind-fields.php:42
     209#: includes/class-kind-fields.php:54
    202210msgid "(GMT-07:00) Mountain Time - Edmonton"
    203211msgstr ""
    204212
    205 #: includes/class-kind-fields.php:43
     213#: includes/class-kind-fields.php:55
    206214msgid "(GMT-07:00) Mountain Time - Hermosillo"
    207215msgstr ""
    208216
    209 #: includes/class-kind-fields.php:44
     217#: includes/class-kind-fields.php:56
    210218msgid "(GMT-07:00) Mountain Time - Chihuahua, Mazatlan"
    211219msgstr ""
    212220
    213 #: includes/class-kind-fields.php:45
     221#: includes/class-kind-fields.php:57
    214222msgid "(GMT-07:00) Mountain Time - Arizona"
    215223msgstr ""
    216224
    217 #: includes/class-kind-fields.php:46
     225#: includes/class-kind-fields.php:58
    218226msgid "(GMT-07:00) Mountain Time - Yellowknife"
    219227msgstr ""
    220228
    221 #: includes/class-kind-fields.php:47
     229#: includes/class-kind-fields.php:59
    222230msgid "(GMT-06:00) Belize"
    223231msgstr ""
    224232
    225 #: includes/class-kind-fields.php:48
     233#: includes/class-kind-fields.php:60
    226234msgid "(GMT-06:00) Central Time"
    227235msgstr ""
    228236
    229 #: includes/class-kind-fields.php:49
     237#: includes/class-kind-fields.php:61
    230238msgid "(GMT-06:00) Costa Rica"
    231239msgstr ""
    232240
    233 #: includes/class-kind-fields.php:50
     241#: includes/class-kind-fields.php:62
    234242msgid "(GMT-06:00) El Salvador"
    235243msgstr ""
    236244
    237 #: includes/class-kind-fields.php:51
     245#: includes/class-kind-fields.php:63
    238246msgid "(GMT-06:00) Guatemala"
    239247msgstr ""
    240248
    241 #: includes/class-kind-fields.php:52
     249#: includes/class-kind-fields.php:64
    242250msgid "(GMT-06:00) Managua"
    243251msgstr ""
    244252
    245 #: includes/class-kind-fields.php:53
     253#: includes/class-kind-fields.php:65
    246254msgid "(GMT-06:00) Central Time - Mexico City"
    247255msgstr ""
    248256
    249 #: includes/class-kind-fields.php:54
     257#: includes/class-kind-fields.php:66
    250258msgid "(GMT-06:00) Central Time - Regina"
    251259msgstr ""
    252260
    253 #: includes/class-kind-fields.php:55
     261#: includes/class-kind-fields.php:67
    254262msgid "(GMT-06:00) Central Time - Tegucigalpa"
    255263msgstr ""
    256264
    257 #: includes/class-kind-fields.php:56
     265#: includes/class-kind-fields.php:68
    258266msgid "(GMT-06:00) Central Time - Winnipeg"
    259267msgstr ""
    260268
    261 #: includes/class-kind-fields.php:57
     269#: includes/class-kind-fields.php:69
    262270msgid "(GMT-06:00) Galapagos"
    263271msgstr ""
    264272
    265 #: includes/class-kind-fields.php:58
     273#: includes/class-kind-fields.php:70
    266274msgid "(GMT-05:00) Bogota"
    267275msgstr ""
    268276
    269 #: includes/class-kind-fields.php:59
     277#: includes/class-kind-fields.php:71
    270278msgid "(GMT-05:00) America Cancun"
    271279msgstr ""
    272280
    273 #: includes/class-kind-fields.php:60
     281#: includes/class-kind-fields.php:72
    274282msgid "(GMT-05:00) Cayman"
    275283msgstr ""
    276284
    277 #: includes/class-kind-fields.php:61
     285#: includes/class-kind-fields.php:73
    278286msgid "(GMT-05:00) Guayaquil"
    279287msgstr ""
    280288
    281 #: includes/class-kind-fields.php:62
     289#: includes/class-kind-fields.php:74
    282290msgid "(GMT-05:00) Havana"
    283291msgstr ""
    284292
    285 #: includes/class-kind-fields.php:63
     293#: includes/class-kind-fields.php:75
    286294msgid "(GMT-05:00) Eastern Time - Iqaluit"
    287295msgstr ""
    288296
    289 #: includes/class-kind-fields.php:64
     297#: includes/class-kind-fields.php:76
    290298msgid "(GMT-05:00) Jamaica"
    291299msgstr ""
    292300
    293 #: includes/class-kind-fields.php:65
     301#: includes/class-kind-fields.php:77
    294302msgid "(GMT-05:00) Lima"
    295303msgstr ""
    296304
    297 #: includes/class-kind-fields.php:66
     305#: includes/class-kind-fields.php:78
    298306msgid "(GMT-05:00) Nassau"
    299307msgstr ""
    300308
    301 #: includes/class-kind-fields.php:67
     309#: includes/class-kind-fields.php:79
    302310msgid "(GMT-05:00) Eastern Time"
    303311msgstr ""
    304312
    305 #: includes/class-kind-fields.php:68
     313#: includes/class-kind-fields.php:80
    306314msgid "(GMT-05:00) Panama"
    307315msgstr ""
    308316
    309 #: includes/class-kind-fields.php:69
     317#: includes/class-kind-fields.php:81
    310318msgid "(GMT-05:00) Port-au-Prince"
    311319msgstr ""
    312320
    313 #: includes/class-kind-fields.php:70
     321#: includes/class-kind-fields.php:82
    314322msgid "(GMT-05:00) Rio Branco"
    315323msgstr ""
    316324
    317 #: includes/class-kind-fields.php:71
     325#: includes/class-kind-fields.php:83
    318326msgid "(GMT-05:00) Eastern Time - Toronto"
    319327msgstr ""
    320328
    321 #: includes/class-kind-fields.php:72
     329#: includes/class-kind-fields.php:84
    322330msgid "(GMT-05:00) Easter Island"
    323331msgstr ""
    324332
    325 #: includes/class-kind-fields.php:73
     333#: includes/class-kind-fields.php:85
    326334msgid "(GMT-04:30) Caracas"
    327335msgstr ""
    328336
    329 #: includes/class-kind-fields.php:74
     337#: includes/class-kind-fields.php:86
    330338msgid "(GMT-03:00) Asuncion"
    331339msgstr ""
    332340
    333 #: includes/class-kind-fields.php:75
     341#: includes/class-kind-fields.php:87
    334342msgid "(GMT-04:00) Barbados"
    335343msgstr ""
    336344
    337 #: includes/class-kind-fields.php:76
     345#: includes/class-kind-fields.php:88
    338346msgid "(GMT-04:00) Boa Vista"
    339347msgstr ""
    340348
    341 #: includes/class-kind-fields.php:77
     349#: includes/class-kind-fields.php:89
    342350msgid "(GMT-03:00) Campo Grande"
    343351msgstr ""
    344352
    345 #: includes/class-kind-fields.php:78
     353#: includes/class-kind-fields.php:90
    346354msgid "(GMT-03:00) Cuiaba"
    347355msgstr ""
    348356
    349 #: includes/class-kind-fields.php:79
     357#: includes/class-kind-fields.php:91
    350358msgid "(GMT-04:00) Curacao"
    351359msgstr ""
    352360
    353 #: includes/class-kind-fields.php:80
     361#: includes/class-kind-fields.php:92
    354362msgid "(GMT-04:00) Grand Turk"
    355363msgstr ""
    356364
    357 #: includes/class-kind-fields.php:81
     365#: includes/class-kind-fields.php:93
    358366msgid "(GMT-04:00) Guyana"
    359367msgstr ""
    360368
    361 #: includes/class-kind-fields.php:82
     369#: includes/class-kind-fields.php:94
    362370msgid "(GMT-04:00) Atlantic Time - Halifax"
    363371msgstr ""
    364372
    365 #: includes/class-kind-fields.php:83
     373#: includes/class-kind-fields.php:95
    366374msgid "(GMT-04:00) La Paz"
    367375msgstr ""
    368376
    369 #: includes/class-kind-fields.php:84
     377#: includes/class-kind-fields.php:96
    370378msgid "(GMT-04:00) Manaus"
    371379msgstr ""
    372380
    373 #: includes/class-kind-fields.php:85
     381#: includes/class-kind-fields.php:97
    374382msgid "(GMT-04:00) Martinique"
    375383msgstr ""
    376384
    377 #: includes/class-kind-fields.php:86
     385#: includes/class-kind-fields.php:98
    378386msgid "(GMT-04:00) Port of Spain"
    379387msgstr ""
    380388
    381 #: includes/class-kind-fields.php:87
     389#: includes/class-kind-fields.php:99
    382390msgid "(GMT-04:00) Porto Velho"
    383391msgstr ""
    384392
    385 #: includes/class-kind-fields.php:88
     393#: includes/class-kind-fields.php:100
    386394msgid "(GMT-04:00) Puerto Rico"
    387395msgstr ""
    388396
    389 #: includes/class-kind-fields.php:89
     397#: includes/class-kind-fields.php:101
    390398msgid "(GMT-04:00) Santo Domingo"
    391399msgstr ""
    392400
    393 #: includes/class-kind-fields.php:90
     401#: includes/class-kind-fields.php:102
    394402msgid "(GMT-04:00) Thule"
    395403msgstr ""
    396404
    397 #: includes/class-kind-fields.php:91
     405#: includes/class-kind-fields.php:103
    398406msgid "(GMT-04:00) Bermuda"
    399407msgstr ""
    400408
    401 #: includes/class-kind-fields.php:92
     409#: includes/class-kind-fields.php:104
    402410msgid "(GMT-03:30) Newfoundland Time - St. Johns"
    403411msgstr ""
    404412
    405 #: includes/class-kind-fields.php:93
     413#: includes/class-kind-fields.php:105
    406414msgid "(GMT-03:00) Araguaina"
    407415msgstr ""
    408416
    409 #: includes/class-kind-fields.php:94
     417#: includes/class-kind-fields.php:106
    410418msgid "(GMT-03:00) Buenos Aires"
    411419msgstr ""
    412420
    413 #: includes/class-kind-fields.php:95
     421#: includes/class-kind-fields.php:107
    414422msgid "(GMT-03:00) Salvador"
    415423msgstr ""
    416424
    417 #: includes/class-kind-fields.php:96
     425#: includes/class-kind-fields.php:108
    418426msgid "(GMT-03:00) Belem"
    419427msgstr ""
    420428
    421 #: includes/class-kind-fields.php:97
     429#: includes/class-kind-fields.php:109
    422430msgid "(GMT-03:00) Cayenne"
    423431msgstr ""
    424432
    425 #: includes/class-kind-fields.php:98
     433#: includes/class-kind-fields.php:110
    426434msgid "(GMT-03:00) Fortaleza"
    427435msgstr ""
    428436
    429 #: includes/class-kind-fields.php:99
     437#: includes/class-kind-fields.php:111
    430438msgid "(GMT-03:00) Godthab"
    431439msgstr ""
    432440
    433 #: includes/class-kind-fields.php:100
     441#: includes/class-kind-fields.php:112
    434442msgid "(GMT-03:00) Maceio"
    435443msgstr ""
    436444
    437 #: includes/class-kind-fields.php:101
     445#: includes/class-kind-fields.php:113
    438446msgid "(GMT-03:00) Miquelon"
    439447msgstr ""
    440448
    441 #: includes/class-kind-fields.php:102
     449#: includes/class-kind-fields.php:114
    442450msgid "(GMT-03:00) Montevideo"
    443451msgstr ""
    444452
    445 #: includes/class-kind-fields.php:103
     453#: includes/class-kind-fields.php:115
    446454msgid "(GMT-03:00) Paramaribo"
    447455msgstr ""
    448456
    449 #: includes/class-kind-fields.php:104
     457#: includes/class-kind-fields.php:116
    450458msgid "(GMT-03:00) Recife"
    451459msgstr ""
    452460
    453 #: includes/class-kind-fields.php:105
     461#: includes/class-kind-fields.php:117
    454462msgid "(GMT-03:00) Santiago"
    455463msgstr ""
    456464
    457 #: includes/class-kind-fields.php:106
     465#: includes/class-kind-fields.php:118
    458466msgid "(GMT-02:00) Sao Paulo"
    459467msgstr ""
    460468
    461 #: includes/class-kind-fields.php:107
     469#: includes/class-kind-fields.php:119
    462470msgid "(GMT-03:00) Palmer"
    463471msgstr ""
    464472
    465 #: includes/class-kind-fields.php:108
     473#: includes/class-kind-fields.php:120
    466474msgid "(GMT-03:00) Rothera"
    467475msgstr ""
    468476
    469 #: includes/class-kind-fields.php:109
     477#: includes/class-kind-fields.php:121
    470478msgid "(GMT-03:00) Stanley"
    471479msgstr ""
    472480
    473 #: includes/class-kind-fields.php:110
     481#: includes/class-kind-fields.php:122
    474482msgid "(GMT-02:00) Noronha"
    475483msgstr ""
    476484
    477 #: includes/class-kind-fields.php:111
     485#: includes/class-kind-fields.php:123
    478486msgid "(GMT-02:00) South Georgia"
    479487msgstr ""
    480488
    481 #: includes/class-kind-fields.php:112
     489#: includes/class-kind-fields.php:124
    482490msgid "(GMT-01:00) Scoresbysund"
    483491msgstr ""
    484492
    485 #: includes/class-kind-fields.php:113
     493#: includes/class-kind-fields.php:125
    486494msgid "(GMT-01:00) Azores"
    487495msgstr ""
    488496
    489 #: includes/class-kind-fields.php:114
     497#: includes/class-kind-fields.php:126
    490498msgid "(GMT-01:00) Cape Verde"
    491499msgstr ""
    492500
    493 #: includes/class-kind-fields.php:115
     501#: includes/class-kind-fields.php:127
    494502msgid "(GMT+00:00) Abidjan"
    495503msgstr ""
    496504
    497 #: includes/class-kind-fields.php:116
     505#: includes/class-kind-fields.php:128
    498506msgid "(GMT+00:00) Accra"
    499507msgstr ""
    500508
    501 #: includes/class-kind-fields.php:117
     509#: includes/class-kind-fields.php:129
    502510msgid "(GMT+00:00) Bissau"
    503511msgstr ""
    504512
    505 #: includes/class-kind-fields.php:118
     513#: includes/class-kind-fields.php:130
    506514msgid "(GMT+00:00) Casablanca"
    507515msgstr ""
    508516
    509 #: includes/class-kind-fields.php:119
     517#: includes/class-kind-fields.php:131
    510518msgid "(GMT+00:00) El Aaiun"
    511519msgstr ""
    512520
    513 #: includes/class-kind-fields.php:120
     521#: includes/class-kind-fields.php:132
    514522msgid "(GMT+00:00) Monrovia"
    515523msgstr ""
    516524
    517 #: includes/class-kind-fields.php:121
     525#: includes/class-kind-fields.php:133
    518526msgid "(GMT+00:00) Danmarkshavn"
    519527msgstr ""
    520528
    521 #: includes/class-kind-fields.php:122
     529#: includes/class-kind-fields.php:134
    522530msgid "(GMT+00:00) Canary Islands"
    523531msgstr ""
    524532
    525 #: includes/class-kind-fields.php:123
     533#: includes/class-kind-fields.php:135
    526534msgid "(GMT+00:00) Faeroe"
    527535msgstr ""
    528536
    529 #: includes/class-kind-fields.php:124
     537#: includes/class-kind-fields.php:136
    530538msgid "(GMT+00:00) Reykjavik"
    531539msgstr ""
    532540
    533 #: includes/class-kind-fields.php:125
     541#: includes/class-kind-fields.php:137
    534542msgid "(GMT+00:00) GMT (no daylight saving)"
    535543msgstr ""
    536544
    537 #: includes/class-kind-fields.php:126
     545#: includes/class-kind-fields.php:138
    538546msgid "(GMT+00:00) Dublin"
    539547msgstr ""
    540548
    541 #: includes/class-kind-fields.php:127
     549#: includes/class-kind-fields.php:139
    542550msgid "(GMT+00:00) Lisbon"
    543551msgstr ""
    544552
    545 #: includes/class-kind-fields.php:128
     553#: includes/class-kind-fields.php:140
    546554msgid "(GMT+00:00) London"
    547555msgstr ""
    548556
    549 #: includes/class-kind-fields.php:129
     557#: includes/class-kind-fields.php:141
    550558msgid "(GMT+01:00) Algiers"
    551559msgstr ""
    552560
    553 #: includes/class-kind-fields.php:130
     561#: includes/class-kind-fields.php:142
    554562msgid "(GMT+01:00) Ceuta"
    555563msgstr ""
    556564
    557 #: includes/class-kind-fields.php:131
     565#: includes/class-kind-fields.php:143
    558566msgid "(GMT+01:00) Lagos"
    559567msgstr ""
    560568
    561 #: includes/class-kind-fields.php:132
     569#: includes/class-kind-fields.php:144
    562570msgid "(GMT+01:00) Ndjamena"
    563571msgstr ""
    564572
    565 #: includes/class-kind-fields.php:133
     573#: includes/class-kind-fields.php:145
    566574msgid "(GMT+01:00) Tunis"
    567575msgstr ""
    568576
    569 #: includes/class-kind-fields.php:134
     577#: includes/class-kind-fields.php:146
    570578msgid "(GMT+02:00) Windhoek"
    571579msgstr ""
    572580
    573 #: includes/class-kind-fields.php:135
     581#: includes/class-kind-fields.php:147
    574582msgid "(GMT+01:00) Amsterdam"
    575583msgstr ""
    576584
    577 #: includes/class-kind-fields.php:136
     585#: includes/class-kind-fields.php:148
    578586msgid "(GMT+01:00) Andorra"
    579587msgstr ""
    580588
    581 #: includes/class-kind-fields.php:137
     589#: includes/class-kind-fields.php:149
    582590msgid "(GMT+01:00) Central European Time - Belgrade"
    583591msgstr ""
    584592
    585 #: includes/class-kind-fields.php:138
     593#: includes/class-kind-fields.php:150
    586594msgid "(GMT+01:00) Berlin"
    587595msgstr ""
    588596
    589 #: includes/class-kind-fields.php:139
     597#: includes/class-kind-fields.php:151
    590598msgid "(GMT+01:00) Brussels"
    591599msgstr ""
    592600
    593 #: includes/class-kind-fields.php:140
     601#: includes/class-kind-fields.php:152
    594602msgid "(GMT+01:00) Budapest"
    595603msgstr ""
    596604
    597 #: includes/class-kind-fields.php:141
     605#: includes/class-kind-fields.php:153
    598606msgid "(GMT+01:00) Copenhagen"
    599607msgstr ""
    600608
    601 #: includes/class-kind-fields.php:142
     609#: includes/class-kind-fields.php:154
    602610msgid "(GMT+01:00) Gibraltar"
    603611msgstr ""
    604612
    605 #: includes/class-kind-fields.php:143
     613#: includes/class-kind-fields.php:155
    606614msgid "(GMT+01:00) Luxembourg"
    607615msgstr ""
    608616
    609 #: includes/class-kind-fields.php:144
     617#: includes/class-kind-fields.php:156
    610618msgid "(GMT+01:00) Madrid"
    611619msgstr ""
    612620
    613 #: includes/class-kind-fields.php:145
     621#: includes/class-kind-fields.php:157
    614622msgid "(GMT+01:00) Malta"
    615623msgstr ""
    616624
    617 #: includes/class-kind-fields.php:146
     625#: includes/class-kind-fields.php:158
    618626msgid "(GMT+01:00) Monaco"
    619627msgstr ""
    620628
    621 #: includes/class-kind-fields.php:147
     629#: includes/class-kind-fields.php:159
    622630msgid "(GMT+01:00) Oslo"
    623631msgstr ""
    624632
    625 #: includes/class-kind-fields.php:148
     633#: includes/class-kind-fields.php:160
    626634msgid "(GMT+01:00) Paris"
    627635msgstr ""
    628636
    629 #: includes/class-kind-fields.php:149
     637#: includes/class-kind-fields.php:161
    630638msgid "(GMT+01:00) Central European Time - Prague"
    631639msgstr ""
    632640
    633 #: includes/class-kind-fields.php:150
     641#: includes/class-kind-fields.php:162
    634642msgid "(GMT+01:00) Rome"
    635643msgstr ""
    636644
    637 #: includes/class-kind-fields.php:151
     645#: includes/class-kind-fields.php:163
    638646msgid "(GMT+01:00) Stockholm"
    639647msgstr ""
    640648
    641 #: includes/class-kind-fields.php:152
     649#: includes/class-kind-fields.php:164
    642650msgid "(GMT+01:00) Tirane"
    643651msgstr ""
    644652
    645 #: includes/class-kind-fields.php:153
     653#: includes/class-kind-fields.php:165
    646654msgid "(GMT+01:00) Vienna"
    647655msgstr ""
    648656
    649 #: includes/class-kind-fields.php:154
     657#: includes/class-kind-fields.php:166
    650658msgid "(GMT+01:00) Warsaw"
    651659msgstr ""
    652660
    653 #: includes/class-kind-fields.php:155
     661#: includes/class-kind-fields.php:167
    654662msgid "(GMT+01:00) Zurich"
    655663msgstr ""
    656664
    657 #: includes/class-kind-fields.php:156
     665#: includes/class-kind-fields.php:168
    658666msgid "(GMT+02:00) Cairo"
    659667msgstr ""
    660668
    661 #: includes/class-kind-fields.php:157
     669#: includes/class-kind-fields.php:169
    662670msgid "(GMT+02:00) Johannesburg"
    663671msgstr ""
    664672
    665 #: includes/class-kind-fields.php:158
     673#: includes/class-kind-fields.php:170
    666674msgid "(GMT+02:00) Maputo"
    667675msgstr ""
    668676
    669 #: includes/class-kind-fields.php:159
     677#: includes/class-kind-fields.php:171
    670678msgid "(GMT+02:00) Tripoli"
    671679msgstr ""
    672680
    673 #: includes/class-kind-fields.php:160
     681#: includes/class-kind-fields.php:172
    674682msgid "(GMT+02:00) Amman"
    675683msgstr ""
    676684
    677 #: includes/class-kind-fields.php:161
     685#: includes/class-kind-fields.php:173
    678686msgid "(GMT+02:00) Beirut"
    679687msgstr ""
    680688
    681 #: includes/class-kind-fields.php:162
     689#: includes/class-kind-fields.php:174
    682690msgid "(GMT+02:00) Damascus"
    683691msgstr ""
    684692
    685 #: includes/class-kind-fields.php:163
     693#: includes/class-kind-fields.php:175
    686694msgid "(GMT+02:00) Gaza"
    687695msgstr ""
    688696
    689 #: includes/class-kind-fields.php:164
     697#: includes/class-kind-fields.php:176
    690698msgid "(GMT+02:00) Jerusalem"
    691699msgstr ""
    692700
    693 #: includes/class-kind-fields.php:165
     701#: includes/class-kind-fields.php:177
    694702msgid "(GMT+02:00) Nicosia"
    695703msgstr ""
    696704
    697 #: includes/class-kind-fields.php:166
     705#: includes/class-kind-fields.php:178
    698706msgid "(GMT+02:00) Athens"
    699707msgstr ""
    700708
    701 #: includes/class-kind-fields.php:167
     709#: includes/class-kind-fields.php:179
    702710msgid "(GMT+02:00) Bucharest"
    703711msgstr ""
    704712
    705 #: includes/class-kind-fields.php:168
     713#: includes/class-kind-fields.php:180
    706714msgid "(GMT+02:00) Chisinau"
    707715msgstr ""
    708716
    709 #: includes/class-kind-fields.php:169
     717#: includes/class-kind-fields.php:181
    710718msgid "(GMT+02:00) Helsinki"
    711719msgstr ""
    712720
    713 #: includes/class-kind-fields.php:170
     721#: includes/class-kind-fields.php:182
    714722msgid "(GMT+02:00) Istanbul"
    715723msgstr ""
    716724
    717 #: includes/class-kind-fields.php:171
     725#: includes/class-kind-fields.php:183
    718726msgid "(GMT+02:00) Moscow-01 - Kaliningrad"
    719727msgstr ""
    720728
    721 #: includes/class-kind-fields.php:172
     729#: includes/class-kind-fields.php:184
    722730msgid "(GMT+02:00) Kiev"
    723731msgstr ""
    724732
    725 #: includes/class-kind-fields.php:173
     733#: includes/class-kind-fields.php:185
    726734msgid "(GMT+02:00) Riga"
    727735msgstr ""
    728736
    729 #: includes/class-kind-fields.php:174
     737#: includes/class-kind-fields.php:186
    730738msgid "(GMT+02:00) Sofia"
    731739msgstr ""
    732740
    733 #: includes/class-kind-fields.php:175
     741#: includes/class-kind-fields.php:187
    734742msgid "(GMT+02:00) Tallinn"
    735743msgstr ""
    736744
    737 #: includes/class-kind-fields.php:176
     745#: includes/class-kind-fields.php:188
    738746msgid "(GMT+02:00) Vilnius"
    739747msgstr ""
    740748
    741 #: includes/class-kind-fields.php:177
     749#: includes/class-kind-fields.php:189
    742750msgid "(GMT+03:00) Khartoum"
    743751msgstr ""
    744752
    745 #: includes/class-kind-fields.php:178
     753#: includes/class-kind-fields.php:190
    746754msgid "(GMT+03:00) Nairobi"
    747755msgstr ""
    748756
    749 #: includes/class-kind-fields.php:179
     757#: includes/class-kind-fields.php:191
    750758msgid "(GMT+03:00) Syowa"
    751759msgstr ""
    752760
    753 #: includes/class-kind-fields.php:180
     761#: includes/class-kind-fields.php:192
    754762msgid "(GMT+03:00) Baghdad"
    755763msgstr ""
    756764
    757 #: includes/class-kind-fields.php:181
     765#: includes/class-kind-fields.php:193
    758766msgid "(GMT+03:00) Qatar"
    759767msgstr ""
    760768
    761 #: includes/class-kind-fields.php:182
     769#: includes/class-kind-fields.php:194
    762770msgid "(GMT+03:00) Riyadh"
    763771msgstr ""
    764772
    765 #: includes/class-kind-fields.php:183
     773#: includes/class-kind-fields.php:195
    766774msgid "(GMT+03:00) Minsk"
    767775msgstr ""
    768776
    769 #: includes/class-kind-fields.php:184
     777#: includes/class-kind-fields.php:196
    770778msgid "(GMT+03:00) Moscow+00 - Moscow"
    771779msgstr ""
    772780
    773 #: includes/class-kind-fields.php:185
     781#: includes/class-kind-fields.php:197
    774782msgid "(GMT+03:30) Tehran"
    775783msgstr ""
    776784
    777 #: includes/class-kind-fields.php:186
     785#: includes/class-kind-fields.php:198
    778786msgid "(GMT+04:00) Baku"
    779787msgstr ""
    780788
    781 #: includes/class-kind-fields.php:187
     789#: includes/class-kind-fields.php:199
    782790msgid "(GMT+04:00) Dubai"
    783791msgstr ""
    784792
    785 #: includes/class-kind-fields.php:188
     793#: includes/class-kind-fields.php:200
    786794msgid "(GMT+04:00) Tbilisi"
    787795msgstr ""
    788796
    789 #: includes/class-kind-fields.php:189
     797#: includes/class-kind-fields.php:201
    790798msgid "(GMT+04:00) Yerevan"
    791799msgstr ""
    792800
    793 #: includes/class-kind-fields.php:190
     801#: includes/class-kind-fields.php:202
    794802msgid "(GMT+04:00) Moscow+01 - Samara"
    795803msgstr ""
    796804
    797 #: includes/class-kind-fields.php:191
     805#: includes/class-kind-fields.php:203
    798806msgid "(GMT+04:00) Mahe"
    799807msgstr ""
    800808
    801 #: includes/class-kind-fields.php:192
     809#: includes/class-kind-fields.php:204
    802810msgid "(GMT+04:00) Mauritius"
    803811msgstr ""
    804812
    805 #: includes/class-kind-fields.php:193
     813#: includes/class-kind-fields.php:205
    806814msgid "(GMT+04:00) Reunion"
    807815msgstr ""
    808816
    809 #: includes/class-kind-fields.php:194
     817#: includes/class-kind-fields.php:206
    810818msgid "(GMT+04:30) Kabul"
    811819msgstr ""
    812820
    813 #: includes/class-kind-fields.php:195
     821#: includes/class-kind-fields.php:207
    814822msgid "(GMT+05:00) Mawson"
    815823msgstr ""
    816824
    817 #: includes/class-kind-fields.php:196
     825#: includes/class-kind-fields.php:208
    818826msgid "(GMT+05:00) Aqtau"
    819827msgstr ""
    820828
    821 #: includes/class-kind-fields.php:197
     829#: includes/class-kind-fields.php:209
    822830msgid "(GMT+05:00) Aqtobe"
    823831msgstr ""
    824832
    825 #: includes/class-kind-fields.php:198
     833#: includes/class-kind-fields.php:210
    826834msgid "(GMT+05:00) Ashgabat"
    827835msgstr ""
    828836
    829 #: includes/class-kind-fields.php:199
     837#: includes/class-kind-fields.php:211
    830838msgid "(GMT+05:00) Dushanbe"
    831839msgstr ""
    832840
    833 #: includes/class-kind-fields.php:200
     841#: includes/class-kind-fields.php:212
    834842msgid "(GMT+05:00) Karachi"
    835843msgstr ""
    836844
    837 #: includes/class-kind-fields.php:201
     845#: includes/class-kind-fields.php:213
    838846msgid "(GMT+05:00) Tashkent"
    839847msgstr ""
    840848
    841 #: includes/class-kind-fields.php:202
     849#: includes/class-kind-fields.php:214
    842850msgid "(GMT+05:00) Moscow+02 - Yekaterinburg"
    843851msgstr ""
    844852
    845 #: includes/class-kind-fields.php:203
     853#: includes/class-kind-fields.php:215
    846854msgid "(GMT+05:00) Kerguelen"
    847855msgstr ""
    848856
    849 #: includes/class-kind-fields.php:204
     857#: includes/class-kind-fields.php:216
    850858msgid "(GMT+05:00) Maldives"
    851859msgstr ""
    852860
    853 #: includes/class-kind-fields.php:205
     861#: includes/class-kind-fields.php:217
    854862msgid "(GMT+05:30) India Standard Time"
    855863msgstr ""
    856864
    857 #: includes/class-kind-fields.php:206
     865#: includes/class-kind-fields.php:218
    858866msgid "(GMT+05:30) Colombo"
    859867msgstr ""
    860868
    861 #: includes/class-kind-fields.php:207
     869#: includes/class-kind-fields.php:219
    862870msgid "(GMT+05:45) Katmandu"
    863871msgstr ""
    864872
    865 #: includes/class-kind-fields.php:208
     873#: includes/class-kind-fields.php:220
    866874msgid "(GMT+06:00) Vostok"
    867875msgstr ""
    868876
    869 #: includes/class-kind-fields.php:209
     877#: includes/class-kind-fields.php:221
    870878msgid "(GMT+06:00) Almaty"
    871879msgstr ""
    872880
    873 #: includes/class-kind-fields.php:210
     881#: includes/class-kind-fields.php:222
    874882msgid "(GMT+06:00) Bishkek"
    875883msgstr ""
    876884
    877 #: includes/class-kind-fields.php:211
     885#: includes/class-kind-fields.php:223
    878886msgid "(GMT+06:00) Dhaka"
    879887msgstr ""
    880888
    881 #: includes/class-kind-fields.php:212
     889#: includes/class-kind-fields.php:224
    882890msgid "(GMT+06:00) Moscow+03 - Omsk, Novosibirsk"
    883891msgstr ""
    884892
    885 #: includes/class-kind-fields.php:213
     893#: includes/class-kind-fields.php:225
    886894msgid "(GMT+06:00) Thimphu"
    887895msgstr ""
    888896
    889 #: includes/class-kind-fields.php:214
     897#: includes/class-kind-fields.php:226
    890898msgid "(GMT+06:00) Chagos"
    891899msgstr ""
    892900
    893 #: includes/class-kind-fields.php:215
     901#: includes/class-kind-fields.php:227
    894902msgid "(GMT+06:30) Rangoon"
    895903msgstr ""
    896904
    897 #: includes/class-kind-fields.php:216
     905#: includes/class-kind-fields.php:228
    898906msgid "(GMT+06:30) Cocos"
    899907msgstr ""
    900908
    901 #: includes/class-kind-fields.php:217
     909#: includes/class-kind-fields.php:229
    902910msgid "(GMT+07:00) Davis"
    903911msgstr ""
    904912
    905 #: includes/class-kind-fields.php:218
     913#: includes/class-kind-fields.php:230
    906914msgid "(GMT+07:00) Bangkok"
    907915msgstr ""
    908916
    909 #: includes/class-kind-fields.php:219
     917#: includes/class-kind-fields.php:231
    910918msgid "(GMT+07:00) Hovd"
    911919msgstr ""
    912920
    913 #: includes/class-kind-fields.php:220
     921#: includes/class-kind-fields.php:232
    914922msgid "(GMT+07:00) Jakarta"
    915923msgstr ""
    916924
    917 #: includes/class-kind-fields.php:221
     925#: includes/class-kind-fields.php:233
    918926msgid "(GMT+07:00) Moscow+04 - Krasnoyarsk"
    919927msgstr ""
    920928
    921 #: includes/class-kind-fields.php:222
     929#: includes/class-kind-fields.php:234
    922930msgid "(GMT+07:00) Hanoi"
    923931msgstr ""
    924932
    925 #: includes/class-kind-fields.php:223
     933#: includes/class-kind-fields.php:235
    926934msgid "(GMT+07:00) Ho Chi Minh"
    927935msgstr ""
    928936
    929 #: includes/class-kind-fields.php:224
     937#: includes/class-kind-fields.php:236
    930938msgid "(GMT+07:00) Christmas"
    931939msgstr ""
    932940
    933 #: includes/class-kind-fields.php:225
     941#: includes/class-kind-fields.php:237
    934942msgid "(GMT+08:00) Casey"
    935943msgstr ""
    936944
    937 #: includes/class-kind-fields.php:226
     945#: includes/class-kind-fields.php:238
    938946msgid "(GMT+08:00) Brunei"
    939947msgstr ""
    940948
    941 #: includes/class-kind-fields.php:227
     949#: includes/class-kind-fields.php:239
    942950msgid "(GMT+08:00) Choibalsan"
    943951msgstr ""
    944952
    945 #: includes/class-kind-fields.php:228
     953#: includes/class-kind-fields.php:240
    946954msgid "(GMT+08:00) Hong Kong"
    947955msgstr ""
    948956
    949 #: includes/class-kind-fields.php:229
     957#: includes/class-kind-fields.php:241
    950958msgid "(GMT+08:00) Moscow+05 - Irkutsk"
    951959msgstr ""
    952960
    953 #: includes/class-kind-fields.php:230
     961#: includes/class-kind-fields.php:242
    954962msgid "(GMT+08:00) Kuala Lumpur"
    955963msgstr ""
    956964
    957 #: includes/class-kind-fields.php:231
     965#: includes/class-kind-fields.php:243
    958966msgid "(GMT+08:00) Macau"
    959967msgstr ""
    960968
    961 #: includes/class-kind-fields.php:232
     969#: includes/class-kind-fields.php:244
    962970msgid "(GMT+08:00) Makassar"
    963971msgstr ""
    964972
    965 #: includes/class-kind-fields.php:233
     973#: includes/class-kind-fields.php:245
    966974msgid "(GMT+08:00) Manila"
    967975msgstr ""
    968976
    969 #: includes/class-kind-fields.php:234
     977#: includes/class-kind-fields.php:246
    970978msgid "(GMT+08:00) China Time - Beijing"
    971979msgstr ""
    972980
    973 #: includes/class-kind-fields.php:235
     981#: includes/class-kind-fields.php:247
    974982msgid "(GMT+08:00) Singapore"
    975983msgstr ""
    976984
    977 #: includes/class-kind-fields.php:236
     985#: includes/class-kind-fields.php:248
    978986msgid "(GMT+08:00) Taipei"
    979987msgstr ""
    980988
    981 #: includes/class-kind-fields.php:237
     989#: includes/class-kind-fields.php:249
    982990msgid "(GMT+08:00) Ulaanbaatar"
    983991msgstr ""
    984992
    985 #: includes/class-kind-fields.php:238
     993#: includes/class-kind-fields.php:250
    986994msgid "(GMT+08:00) Western Time - Perth"
    987995msgstr ""
    988996
    989 #: includes/class-kind-fields.php:239
     997#: includes/class-kind-fields.php:251
    990998msgid "(GMT+08:30) Pyongyang"
    991999msgstr ""
    9921000
    993 #: includes/class-kind-fields.php:240
     1001#: includes/class-kind-fields.php:252
    9941002msgid "(GMT+09:00) Dili"
    9951003msgstr ""
    9961004
    997 #: includes/class-kind-fields.php:241
     1005#: includes/class-kind-fields.php:253
    9981006msgid "(GMT+09:00) Jayapura"
    9991007msgstr ""
    10001008
    1001 #: includes/class-kind-fields.php:242
     1009#: includes/class-kind-fields.php:254
    10021010msgid "(GMT+09:00) Seoul"
    10031011msgstr ""
    10041012
    1005 #: includes/class-kind-fields.php:243
     1013#: includes/class-kind-fields.php:255
    10061014msgid "(GMT+09:00) Tokyo"
    10071015msgstr ""
    10081016
    1009 #: includes/class-kind-fields.php:244
     1017#: includes/class-kind-fields.php:256
    10101018msgid "(GMT+09:00) Moscow+06 - Yakutsk"
    10111019msgstr ""
    10121020
    1013 #: includes/class-kind-fields.php:245
     1021#: includes/class-kind-fields.php:257
    10141022msgid "(GMT+09:00) Palau"
    10151023msgstr ""
    10161024
    1017 #: includes/class-kind-fields.php:246
     1025#: includes/class-kind-fields.php:258
    10181026msgid "(GMT+10:30) Central Time - Adelaide"
    10191027msgstr ""
    10201028
    1021 #: includes/class-kind-fields.php:247
     1029#: includes/class-kind-fields.php:259
    10221030msgid "(GMT+09:30) Central Time - Darwin"
    10231031msgstr ""
    10241032
    1025 #: includes/class-kind-fields.php:249
     1033#: includes/class-kind-fields.php:261
    10261034msgid "(GMT+10:00) Moscow+07 - Magadan"
    10271035msgstr ""
    10281036
    1029 #: includes/class-kind-fields.php:250
     1037#: includes/class-kind-fields.php:262
    10301038msgid "(GMT+10:00) Moscow+07 - Yuzhno-Sakhalinsk"
    10311039msgstr ""
    10321040
    1033 #: includes/class-kind-fields.php:251
     1041#: includes/class-kind-fields.php:263
    10341042msgid "(GMT+10:00) Eastern Time - Brisbane"
    10351043msgstr ""
    10361044
    1037 #: includes/class-kind-fields.php:252
     1045#: includes/class-kind-fields.php:264
    10381046msgid "(GMT+11:00) Eastern Time - Hobart"
    10391047msgstr ""
    10401048
    1041 #: includes/class-kind-fields.php:253
     1049#: includes/class-kind-fields.php:265
    10421050msgid "(GMT+11:00) Eastern Time - Melbourne, Sydney"
    10431051msgstr ""
    10441052
    1045 #: includes/class-kind-fields.php:254
     1053#: includes/class-kind-fields.php:266
    10461054msgid "(GMT+10:00) Truk"
    10471055msgstr ""
    10481056
    1049 #: includes/class-kind-fields.php:255
     1057#: includes/class-kind-fields.php:267
    10501058msgid "(GMT+10:00) Guam"
    10511059msgstr ""
    10521060
    1053 #: includes/class-kind-fields.php:256
     1061#: includes/class-kind-fields.php:268
    10541062msgid "(GMT+10:00) Port Moresby"
    10551063msgstr ""
    10561064
    1057 #: includes/class-kind-fields.php:257
     1065#: includes/class-kind-fields.php:269
    10581066msgid "(GMT+11:00) Efate"
    10591067msgstr ""
    10601068
    1061 #: includes/class-kind-fields.php:258
     1069#: includes/class-kind-fields.php:270
    10621070msgid "(GMT+11:00) Guadalcanal"
    10631071msgstr ""
    10641072
    1065 #: includes/class-kind-fields.php:259
     1073#: includes/class-kind-fields.php:271
    10661074msgid "(GMT+11:00) Kosrae"
    10671075msgstr ""
    10681076
    1069 #: includes/class-kind-fields.php:260
     1077#: includes/class-kind-fields.php:272
    10701078msgid "(GMT+11:00) Norfolk"
    10711079msgstr ""
    10721080
    1073 #: includes/class-kind-fields.php:261
     1081#: includes/class-kind-fields.php:273
    10741082msgid "(GMT+11:00) Noumea"
    10751083msgstr ""
    10761084
    1077 #: includes/class-kind-fields.php:262
     1085#: includes/class-kind-fields.php:274
    10781086msgid "(GMT+11:00) Ponape"
    10791087msgstr ""
    10801088
    1081 #: includes/class-kind-fields.php:263
     1089#: includes/class-kind-fields.php:275
    10821090msgid "(GMT+12:00) Moscow+09 - Petropavlovsk-Kamchatskiy"
    10831091msgstr ""
    10841092
    1085 #: includes/class-kind-fields.php:264
     1093#: includes/class-kind-fields.php:276
    10861094msgid "(GMT+13:00) Auckland"
    10871095msgstr ""
    10881096
    1089 #: includes/class-kind-fields.php:265
     1097#: includes/class-kind-fields.php:277
    10901098msgid "(GMT+13:00) Fiji"
    10911099msgstr ""
    10921100
    1093 #: includes/class-kind-fields.php:266
     1101#: includes/class-kind-fields.php:278
    10941102msgid "(GMT+12:00) Funafuti"
    10951103msgstr ""
    10961104
    1097 #: includes/class-kind-fields.php:267
     1105#: includes/class-kind-fields.php:279
    10981106msgid "(GMT+12:00) Kwajalein"
    10991107msgstr ""
    11001108
    1101 #: includes/class-kind-fields.php:268
     1109#: includes/class-kind-fields.php:280
    11021110msgid "(GMT+12:00) Majuro"
    11031111msgstr ""
    11041112
    1105 #: includes/class-kind-fields.php:269
     1113#: includes/class-kind-fields.php:281
    11061114msgid "(GMT+12:00) Nauru"
    11071115msgstr ""
    11081116
    1109 #: includes/class-kind-fields.php:270
     1117#: includes/class-kind-fields.php:282
    11101118msgid "(GMT+12:00) Tarawa"
    11111119msgstr ""
    11121120
    1113 #: includes/class-kind-fields.php:271
     1121#: includes/class-kind-fields.php:283
    11141122msgid "(GMT+12:00) Wake"
    11151123msgstr ""
    11161124
    1117 #: includes/class-kind-fields.php:272
     1125#: includes/class-kind-fields.php:284
    11181126msgid "(GMT+12:00) Wallis"
    11191127msgstr ""
    11201128
    1121 #: includes/class-kind-fields.php:273
     1129#: includes/class-kind-fields.php:285
    11221130msgid "(GMT+14:00) Apia"
    11231131msgstr ""
    11241132
    1125 #: includes/class-kind-fields.php:274
     1133#: includes/class-kind-fields.php:286
    11261134msgid "(GMT+13:00) Enderbury"
    11271135msgstr ""
    11281136
    1129 #: includes/class-kind-fields.php:275
     1137#: includes/class-kind-fields.php:287
    11301138msgid "(GMT+13:00) Fakaofo"
    11311139msgstr ""
    11321140
    1133 #: includes/class-kind-fields.php:276
     1141#: includes/class-kind-fields.php:288
    11341142msgid "(GMT+13:00) Tongatapu"
    11351143msgstr ""
    11361144
    1137 #: includes/class-kind-fields.php:277
     1145#: includes/class-kind-fields.php:289
    11381146msgid "(GMT+14:00) Kiritimati"
    11391147msgstr ""
    11401148
    1141 #: includes/class-kind-fields.php:385
     1149#: includes/class-kind-fields.php:396
    11421150msgid "Author Name"
    11431151msgstr ""
    11441152
    1145 #: includes/class-kind-fields.php:389
    1146 #: templates/reply-author.php:12
     1153#: includes/class-kind-fields.php:400
     1154#: templates/reply-author.php:26
    11471155msgid "Author URL"
    11481156msgstr ""
    11491157
    1150 #: includes/class-kind-fields.php:393
     1158#: includes/class-kind-fields.php:404
    11511159msgid "Author Photo"
    11521160msgstr ""
    11531161
    1154 #: includes/class-kind-media-metadata.php:66
    1155 #: includes/register-kinds.php:542
    1156 #: includes/register-kinds.php:607
     1162#: includes/class-kind-media-metadata.php:125
     1163#: includes/register-kinds.php:551
     1164#: includes/register-kinds.php:616
    11571165msgid "Artist"
    11581166msgstr ""
    11591167
    1160 #: includes/class-kind-media-metadata.php:67
     1168#: includes/class-kind-media-metadata.php:126
    11611169msgid "The creator of the image"
    11621170msgstr ""
    11631171
    1164 #: includes/class-kind-media-metadata.php:72
     1172#: includes/class-kind-media-metadata.php:131
    11651173msgid "Artist URL"
    11661174msgstr ""
    11671175
    1168 #: includes/class-kind-media-metadata.php:73
     1176#: includes/class-kind-media-metadata.php:132
    11691177msgid "The Artists URL or webpage"
    11701178msgstr ""
    11711179
    1172 #: includes/class-kind-menu-widget.php:10
     1180#: includes/class-kind-menu-widget.php:25
    11731181msgid "Kind Menu Widget"
    11741182msgstr ""
    11751183
    1176 #: includes/class-kind-menu-widget.php:13
     1184#: includes/class-kind-menu-widget.php:28
    11771185msgid "A widget that allows you to display a menu of kind archives"
    11781186msgstr ""
    11791187
    11801188#. translators: 1. Blog Title 2. Kind Singular Name
    1181 #: includes/class-kind-menu-widget.php:62
    1182 msgid "title=\"%1$s %2$s Feed\""
    1183 msgstr ""
    1184 
    1185 #: includes/class-kind-menu-widget.php:78
     1189#: includes/class-kind-menu-widget.php:80
     1190#, php-format
     1191msgid "%1$s %2$s Feed"
     1192msgstr ""
     1193
     1194#: includes/class-kind-menu-widget.php:96
    11861195msgid "All Posts"
    11871196msgstr ""
    11881197
    1189 #: includes/class-kind-menu-widget.php:122
    1190 #: includes/class-kind-post-widget.php:95
     1198#: includes/class-kind-menu-widget.php:149
     1199#: includes/class-kind-post-widget.php:118
    11911200msgid "Title: "
    11921201msgstr ""
    11931202
    1194 #: includes/class-kind-menu-widget.php:143
     1203#: includes/class-kind-menu-widget.php:169
    11951204msgid "Show Link to All:"
    11961205msgstr ""
    11971206
    1198 #: includes/class-kind-menu-widget.php:148
     1207#: includes/class-kind-menu-widget.php:174
    11991208msgid "Show Count:"
    12001209msgstr ""
    12011210
    1202 #: includes/class-kind-metabox.php:154
     1211#: includes/class-kind-metabox.php:211
    12031212msgid "Your URL has been successfully retrieved and parsed"
    12041213msgstr ""
    12051214
    1206 #: includes/class-kind-metabox.php:155
     1215#: includes/class-kind-metabox.php:212
    12071216msgid "Are you sure you want to clear post properties?"
    12081217msgstr ""
    12091218
    1210 #: includes/class-kind-metabox.php:272
     1219#: includes/class-kind-metabox.php:213
     1220msgid "Error: Unable to Retrieve"
     1221msgstr ""
     1222
     1223#: includes/class-kind-metabox.php:214
     1224msgid "Invalid URL"
     1225msgstr ""
     1226
     1227#: includes/class-kind-metabox.php:215
     1228msgid "Attach"
     1229msgstr ""
     1230
     1231#: includes/class-kind-metabox.php:216
     1232msgid "Use this media"
     1233msgstr ""
     1234
     1235#: includes/class-kind-metabox.php:346
    12111236msgid "Very Dissatisfied"
    12121237msgstr ""
    12131238
    1214 #: includes/class-kind-metabox.php:273
     1239#: includes/class-kind-metabox.php:347
    12151240msgid "Dissatisfied"
    12161241msgstr ""
    12171242
    1218 #: includes/class-kind-metabox.php:274
     1243#: includes/class-kind-metabox.php:348
    12191244msgid "Neutral"
    12201245msgstr ""
    12211246
    1222 #: includes/class-kind-metabox.php:275
     1247#: includes/class-kind-metabox.php:349
    12231248msgid "Satisfied"
    12241249msgstr ""
    12251250
    1226 #: includes/class-kind-metabox.php:276
     1251#: includes/class-kind-metabox.php:350
    12271252msgid "Very Satisfied"
    12281253msgstr ""
    12291254
    1230 #: includes/class-kind-metabox.php:300
    1231 #: includes/register-kinds.php:459
     1255#: includes/class-kind-metabox.php:376
     1256#: includes/register-kinds.php:468
    12321257msgid "Yes"
    12331258msgstr ""
    12341259
    1235 #: includes/class-kind-metabox.php:301
     1260#: includes/class-kind-metabox.php:377
     1261#: includes/register-kinds.php:469
     1262msgid "No"
     1263msgstr ""
     1264
     1265#: includes/class-kind-metabox.php:378
     1266#: includes/register-kinds.php:470
     1267msgid "Maybe"
     1268msgstr ""
     1269
     1270#: includes/class-kind-metabox.php:379
     1271#: includes/register-kinds.php:471
     1272msgid "Interested"
     1273msgstr ""
     1274
     1275#: includes/class-kind-metabox.php:380
     1276#: includes/register-kinds.php:472
     1277msgid "Remote"
     1278msgstr ""
     1279
     1280#: includes/class-kind-metabox.php:404
    12361281#: includes/register-kinds.php:460
    1237 msgid "No"
    1238 msgstr ""
    1239 
    1240 #: includes/class-kind-metabox.php:302
    1241 #: includes/register-kinds.php:461
    1242 msgid "Maybe"
    1243 msgstr ""
    1244 
    1245 #: includes/class-kind-metabox.php:303
    1246 #: includes/register-kinds.php:462
    1247 msgid "Interested"
    1248 msgstr ""
    1249 
    1250 #: includes/class-kind-metabox.php:304
    1251 #: includes/register-kinds.php:463
    1252 msgid "Remote"
    1253 msgstr ""
    1254 
    1255 #: includes/class-kind-metabox.php:326
    1256 #: includes/register-kinds.php:451
     1282#: views/kind.php:81
    12571283msgid "RSVP"
    12581284msgstr ""
    12591285
    1260 #: includes/class-kind-metabox.php:342
     1286#: includes/class-kind-metabox.php:422
    12611287msgid "Rating"
    12621288msgstr ""
    12631289
    1264 #: includes/class-kind-metabox.php:357
     1290#: includes/class-kind-metabox.php:461
    12651291msgid "Response Properties"
    12661292msgstr ""
    12671293
    1268 #: includes/class-kind-plugins.php:133
     1294#: includes/class-kind-plugins.php:209
    12691295msgctxt "direct article"
    12701296msgid "this"
    12711297msgstr ""
    12721298
    1273 #: includes/class-kind-post-widget.php:10
     1299#: includes/class-kind-plugins.php:535
     1300msgid "Nothing could be parsed from the URL."
     1301msgstr ""
     1302
     1303#: includes/class-kind-post-widget.php:25
    12741304msgid "Kind Post Widget"
    12751305msgstr ""
    12761306
    1277 #: includes/class-kind-post-widget.php:13
     1307#: includes/class-kind-post-widget.php:28
    12781308msgid "A widget that allows you to display a list of posts by type"
    12791309msgstr ""
    12801310
    1281 #: includes/class-kind-post-widget.php:63
    1282 msgid "No Posts Found"
    1283 msgstr ""
    1284 
    1285 #: includes/class-kind-post-widget.php:115
     1311#: includes/class-kind-post-widget.php:137
    12861312msgid "Number of Posts:"
    12871313msgstr ""
    12881314
    1289 #: includes/class-kind-taxonomy.php:305
    1290 msgid "Post Kind"
    1291 msgstr ""
    1292 
    1293 #: includes/class-kind-taxonomy.php:526
     1315#: includes/class-kind-taxonomy.php:161
     1316msgid "Unknown post kind."
     1317msgstr ""
     1318
     1319#: includes/class-kind-taxonomy.php:363
     1320msgid "The post kind, or false if the post has none."
     1321msgstr ""
     1322
     1323#: includes/class-kind-taxonomy.php:609
     1324msgid "Post Kinds replaces the IndieWeb Taxonomy plugin. Deactivate IndieWeb Taxonomy, then activate Post Kinds."
     1325msgstr ""
     1326
     1327#: includes/class-kind-taxonomy.php:697
    12941328msgctxt "taxonomy general name"
    12951329msgid "Kinds"
    12961330msgstr ""
    12971331
    1298 #: includes/class-kind-taxonomy.php:527
     1332#: includes/class-kind-taxonomy.php:698
    12991333msgctxt "taxonomy singular name"
    13001334msgid "Kind"
    13011335msgstr ""
    13021336
    1303 #: includes/class-kind-taxonomy.php:528
     1337#: includes/class-kind-taxonomy.php:699
    13041338msgctxt "search locations"
    13051339msgid "Search Kinds"
    13061340msgstr ""
    13071341
    1308 #: includes/class-kind-taxonomy.php:529
     1342#: includes/class-kind-taxonomy.php:700
    13091343msgctxt "popular kinds"
    13101344msgid "Popular Kinds"
    13111345msgstr ""
    13121346
    1313 #: includes/class-kind-taxonomy.php:530
     1347#: includes/class-kind-taxonomy.php:701
    13141348msgctxt "all taxonomy items"
    13151349msgid "All Kinds"
    13161350msgstr ""
    13171351
    1318 #: includes/class-kind-taxonomy.php:531
     1352#: includes/class-kind-taxonomy.php:702
    13191353msgctxt "taxonomy parent item"
    13201354msgid "Parent Kind"
    13211355msgstr ""
    13221356
    1323 #: includes/class-kind-taxonomy.php:532
     1357#: includes/class-kind-taxonomy.php:703
    13241358msgctxt "taxonomy parent item with colon"
    13251359msgid "Parent Kind:"
    13261360msgstr ""
    13271361
    1328 #: includes/class-kind-taxonomy.php:533
     1362#: includes/class-kind-taxonomy.php:704
    13291363msgid "This will appear on archives as the description if your theme supports"
    13301364msgstr ""
    13311365
    1332 #: includes/class-kind-taxonomy.php:534
     1366#: includes/class-kind-taxonomy.php:705
    13331367msgctxt "edit taxonomy item"
    13341368msgid "Edit Kind"
    13351369msgstr ""
    13361370
    1337 #: includes/class-kind-taxonomy.php:535
     1371#: includes/class-kind-taxonomy.php:706
    13381372msgctxt "view taxonomy item"
    13391373msgid "View Kind"
    13401374msgstr ""
    13411375
    1342 #: includes/class-kind-taxonomy.php:536
     1376#: includes/class-kind-taxonomy.php:707
    13431377msgctxt "update taxonomy item"
    13441378msgid "Update Kind"
    13451379msgstr ""
    13461380
    1347 #: includes/class-kind-taxonomy.php:537
     1381#: includes/class-kind-taxonomy.php:708
    13481382msgctxt "add taxonomy item"
    13491383msgid "Add New Kind"
    13501384msgstr ""
    13511385
    1352 #: includes/class-kind-taxonomy.php:538
     1386#: includes/class-kind-taxonomy.php:709
    13531387msgctxt "new taxonomy item"
    13541388msgid "New Kind"
    13551389msgstr ""
    13561390
    1357 #: includes/class-kind-taxonomy.php:539
     1391#: includes/class-kind-taxonomy.php:710
    13581392msgctxt "separate kinds with commas"
    13591393msgid "Separate kinds with commas"
    13601394msgstr ""
    13611395
    1362 #: includes/class-kind-taxonomy.php:540
     1396#: includes/class-kind-taxonomy.php:711
    13631397msgctxt "add or remove items"
    13641398msgid "Add or remove kinds"
    13651399msgstr ""
    13661400
    1367 #: includes/class-kind-taxonomy.php:541
     1401#: includes/class-kind-taxonomy.php:712
    13681402msgctxt "choose most used"
    13691403msgid "Choose from the most used kinds"
    13701404msgstr ""
    13711405
    1372 #: includes/class-kind-taxonomy.php:542
     1406#: includes/class-kind-taxonomy.php:713
    13731407msgctxt "no kinds found"
    13741408msgid "No kinds found"
    13751409msgstr ""
    13761410
    1377 #: includes/class-kind-taxonomy.php:543
     1411#: includes/class-kind-taxonomy.php:714
    13781412msgctxt "no kinds"
    13791413msgid "No kinds"
    13801414msgstr ""
    13811415
    1382 #: includes/class-kind-taxonomy.php:544
     1416#: includes/class-kind-taxonomy.php:715
    13831417msgid "Back to Kinds"
    13841418msgstr ""
    13851419
    1386 #: includes/class-kind-taxonomy.php:545
     1420#: includes/class-kind-taxonomy.php:716
    13871421msgid "Kind Link"
    13881422msgstr ""
    13891423
    1390 #: includes/class-kind-taxonomy.php:546
     1424#: includes/class-kind-taxonomy.php:717
    13911425msgid "A link to a kind"
    1392 msgstr ""
    1393 
    1394 #: includes/class-kind-taxonomy.php:565
    1395 #: includes/register-kinds.php:8
    1396 msgid "Article"
    13971426msgstr ""
    13981427
    … …  
    14001429#. translators: 1: Taxonomy. 2: Month name and year
    14011430#. translators: 1. Taxonomy . 1: Date
    1402 #: includes/class-kind-taxonomy.php:860
    1403 #: includes/class-kind-taxonomy.php:863
    1404 #: includes/class-kind-taxonomy.php:866
     1431#: includes/class-kind-taxonomy.php:1114
     1432#: includes/class-kind-taxonomy.php:1117
     1433#: includes/class-kind-taxonomy.php:1120
     1434#, php-format
    14051435msgid "%1$1s - %2$2s"
    14061436msgstr ""
    14071437
    14081438#. translators: 1: Taxonomy. Yearly archive title. 2: Year
    1409 #: includes/class-kind-taxonomy.php:860
     1439#: includes/class-kind-taxonomy.php:1114
    14101440msgctxt "yearly archives date format"
    14111441msgid "Y"
    … …  
    14131443
    14141444#. translators: 1: Taxonomy. 2: Month name and year
    1415 #: includes/class-kind-taxonomy.php:863
     1445#: includes/class-kind-taxonomy.php:1117
    14161446msgctxt "monthly archives date format"
    14171447msgid "F Y"
    … …  
    14191449
    14201450#. translators: 1. Taxonomy . 1: Date
    1421 #: includes/class-kind-taxonomy.php:866
     1451#: includes/class-kind-taxonomy.php:1120
    14221452msgctxt "daily archives date format"
    14231453msgid "F j, Y"
    … …  
    14251455
    14261456#. translators: 1. Title 2. Translated term for Firehose Feed
    1427 #: includes/class-kind-taxonomy.php:870
     1457#: includes/class-kind-taxonomy.php:1124
     1458#, php-format
    14281459msgid "%1$s - %2$2s"
    14291460msgstr ""
    14301461
    14311462#. translators: 1. Title 2. Translated term for Firehose Feed
    1432 #: includes/class-kind-taxonomy.php:870
     1463#: includes/class-kind-taxonomy.php:1124
    14331464msgid "Firehose"
    14341465msgstr ""
    14351466
    14361467#. translators: 1: Title prefix. 2: Title.
    1437 #: includes/class-kind-taxonomy.php:876
     1468#: includes/class-kind-taxonomy.php:1130
     1469#, php-format
    14381470msgctxt "archive title"
    14391471msgid "%1$s %2$s"
    … …  
    14411473
    14421474#. translators: All
    1443 #: includes/class-kind-taxonomy.php:1164
     1475#: includes/class-kind-taxonomy.php:1455
     1476#, php-format
    14441477msgid "All %1s"
    14451478msgstr ""
    14461479
    1447 #: includes/class-kind-taxonomy.php:1334
     1480#: includes/class-kind-taxonomy.php:1647
    14481481msgid "Invalid post"
    14491482msgstr ""
    14501483
    1451 #: includes/class-kind-taxonomy.php:1337
     1484#: includes/class-kind-taxonomy.php:1650
    14521485msgid "Invalid Kind"
    14531486msgstr ""
    14541487
    1455 #: includes/class-kind-view.php:418
     1488#: includes/class-kind-taxonomy.php:1670
     1489msgid "Invalid post."
     1490msgstr ""
     1491
     1492#: includes/class-kind-taxonomy.php:1673
     1493msgid "Sorry, you are not allowed to set the kind of this post."
     1494msgstr ""
     1495
     1496#: includes/class-kind-view.php:495
    14561497msgctxt "singular Twitter"
    14571498msgid "a tweet"
    14581499msgstr ""
    14591500
    1460 #: includes/class-kind-view.php:419
     1501#: includes/class-kind-view.php:496
    14611502msgctxt "singular Vimeo"
    14621503msgid "a video"
    14631504msgstr ""
    14641505
    1465 #: includes/class-kind-view.php:420
     1506#: includes/class-kind-view.php:497
    14661507msgctxt "singular Youtube"
    14671508msgid "a video"
    14681509msgstr ""
    14691510
    1470 #: includes/class-kind-view.php:421
     1511#: includes/class-kind-view.php:498
    14711512msgctxt "singular Intagram"
    14721513msgid "an image"
    14731514msgstr ""
    14741515
    1475 #: includes/class-kind-view.php:427
     1516#: includes/class-kind-view.php:513
    14761517msgctxt "singular post"
    14771518msgid "a post"
    14781519msgstr ""
    14791520
    1480 #: includes/class-kind-view.php:494
     1521#: includes/class-kind-view.php:589
    14811522msgid "an author"
    14821523msgstr ""
    14831524
    1484 #. translators: URL for link to event and name of event
    1485 #: includes/class-kind-view.php:614
    1486 msgid "Attending <a href=\"%1$s\" class=\"u-in-reply-to\">%2$s</a>"
    1487 msgstr ""
    1488 
    1489 #. translators: URL for link to event and name of event
    1490 #: includes/class-kind-view.php:616
    1491 msgid "Might be attending <a href=\"%1$s\" class=\"u-in-reply-to\">%2$s</a>"
    1492 msgstr ""
    1493 
    1494 #. translators: URL for link to event and name of event
    1495 #: includes/class-kind-view.php:618
    1496 msgid "Unable to Attend <a href=\"%1$s\" class=\"u-in-reply-to\">%2$s</a>"
    1497 msgstr ""
    1498 
    1499 #. translators: URL for link to event and name of event
    1500 #: includes/class-kind-view.php:620
    1501 msgid "Interested in Attending <a href=\"%1$s\" class=u-in-reply-to\">%2$s</a>"
    1502 msgstr ""
    1503 
    1504 #. translators: URL for link to event and name of event
    1505 #: includes/class-kind-view.php:622
    1506 msgid "Attending <a href=\"%1$s\" class=\"u-in-reply-to\">%2$s</a> remotely"
    1507 msgstr ""
    1508 
    1509 #: includes/class-kind-view.php:640
     1525#. translators: %s: Link to the event
     1526#: includes/class-kind-view.php:752
     1527#, php-format
     1528msgid "Attending %s"
     1529msgstr ""
     1530
     1531#. translators: %s: Link to the event
     1532#: includes/class-kind-view.php:754
     1533#, php-format
     1534msgid "Might be attending %s"
     1535msgstr ""
     1536
     1537#. translators: %s: Link to the event
     1538#: includes/class-kind-view.php:756
     1539#, php-format
     1540msgid "Unable to Attend %s"
     1541msgstr ""
     1542
     1543#. translators: %s: Link to the event
     1544#: includes/class-kind-view.php:758
     1545#, php-format
     1546msgid "Interested in Attending %s"
     1547msgstr ""
     1548
     1549#. translators: %s: Link to the event
     1550#: includes/class-kind-view.php:760
     1551#, php-format
     1552msgid "Attending %s remotely"
     1553msgstr ""
     1554
     1555#: includes/class-kind-view.php:794
    15101556msgid "Want to Read: "
    15111557msgstr ""
    15121558
    1513 #: includes/class-kind-view.php:641
     1559#: includes/class-kind-view.php:795
    15141560msgid "Reading: "
    15151561msgstr ""
    15161562
    1517 #: includes/class-kind-view.php:642
     1563#: includes/class-kind-view.php:796
    15181564msgid "Finished Reading: "
    15191565msgstr ""
    15201566
    15211567#. translators: singular and plural
    1522 #: includes/class-kind-view.php:674
     1568#: includes/class-kind-view.php:828
     1569#, php-format
    15231570msgid "%d year"
    15241571msgid_plural "%d years"
    … …  
    15271574
    15281575#. translators: singular and plural
    1529 #: includes/class-kind-view.php:678
     1576#: includes/class-kind-view.php:832
     1577#, php-format
    15301578msgid "%d month"
    15311579msgid_plural "%d months"
    … …  
    15341582
    15351583#. translators: singular and plural
    1536 #: includes/class-kind-view.php:682
     1584#: includes/class-kind-view.php:836
     1585#, php-format
    15371586msgid "%d day"
    15381587msgid_plural "%d days"
    … …  
    15411590
    15421591#. translators: singular and plural
    1543 #: includes/class-kind-view.php:686
     1592#: includes/class-kind-view.php:840
     1593#, php-format
    15441594msgid "%d hour"
    15451595msgid_plural "%d hours"
    … …  
    15481598
    15491599#. translators: singular and plural
    1550 #: includes/class-kind-view.php:690
     1600#: includes/class-kind-view.php:844
     1601#, php-format
    15511602msgid "%d minute"
    15521603msgid_plural "%d minutes"
    … …  
    15551606
    15561607#. translators: singular and plural
    1557 #: includes/class-kind-view.php:694
     1608#: includes/class-kind-view.php:848
     1609#, php-format
    15581610msgid "%d second"
    15591611msgid_plural "%d seconds"
    … …  
    15611613msgstr[1] ""
    15621614
    1563 #: includes/register-kinds.php:9
     1615#: includes/register-kinds.php:17
     1616msgid "Article"
     1617msgstr ""
     1618
     1619#: includes/register-kinds.php:18
    15641620msgid "Articles"
    15651621msgstr ""
    15661622
    1567 #: includes/register-kinds.php:14
     1623#: includes/register-kinds.php:23
    15681624msgid "traditional long form content: a post with an explicit title and body"
    15691625msgstr ""
    15701626
    1571 #: includes/register-kinds.php:25
     1627#: includes/register-kinds.php:34
    15721628msgid "Note"
    15731629msgstr ""
    15741630
    1575 #: includes/register-kinds.php:26
     1631#: includes/register-kinds.php:35
    15761632msgid "Notes"
    15771633msgstr ""
    15781634
    1579 #: includes/register-kinds.php:31
     1635#: includes/register-kinds.php:40
    15801636msgid "short content: a post or status update with just plain content and typically without a title"
    15811637msgstr ""
    15821638
    1583 #: includes/register-kinds.php:42
     1639#: includes/register-kinds.php:51
    15841640msgid "Reply"
    15851641msgstr ""
    15861642
    1587 #: includes/register-kinds.php:43
     1643#: includes/register-kinds.php:52
    15881644msgid "Replies"
    15891645msgstr ""
    15901646
    1591 #: includes/register-kinds.php:44
     1647#: includes/register-kinds.php:53
    15921648msgid "Replied to"
    15931649msgstr ""
    15941650
    1595 #: includes/register-kinds.php:49
     1651#: includes/register-kinds.php:58
    15961652msgid "In Reply To"
    15971653msgstr ""
    15981654
    1599 #: includes/register-kinds.php:53
     1655#: includes/register-kinds.php:62
     1656#: includes/register-kinds.php:131
     1657#: includes/register-kinds.php:200
     1658#: includes/register-kinds.php:269
     1659#: includes/register-kinds.php:338
     1660#: includes/register-kinds.php:481
     1661#: includes/register-kinds.php:543
     1662#: includes/register-kinds.php:608
     1663#: includes/register-kinds.php:673
     1664#: templates/reply-metabox.php:56
     1665msgid "Name"
     1666msgstr ""
     1667
     1668#: includes/register-kinds.php:66
     1669#: includes/register-kinds.php:135
     1670#: includes/register-kinds.php:204
     1671#: includes/register-kinds.php:273
     1672#: includes/register-kinds.php:342
     1673#: includes/register-kinds.php:485
     1674#: includes/register-kinds.php:547
     1675#: includes/register-kinds.php:612
     1676#: includes/register-kinds.php:677
     1677#: templates/reply-metabox.php:50
     1678msgid "URL"
     1679msgstr ""
     1680
     1681#: includes/register-kinds.php:70
     1682#: includes/register-kinds.php:139
     1683#: includes/register-kinds.php:208
     1684#: includes/register-kinds.php:277
     1685#: includes/register-kinds.php:346
     1686#: templates/reply-author.php:20
     1687#: templates/reply-metabox.php:45
     1688msgid "Author"
     1689msgstr ""
     1690
     1691#: includes/register-kinds.php:77
     1692#: includes/register-kinds.php:146
     1693#: includes/register-kinds.php:215
     1694#: includes/register-kinds.php:284
     1695#: includes/register-kinds.php:353
     1696#: includes/register-kinds.php:489
     1697#: includes/register-kinds.php:558
     1698#: includes/register-kinds.php:623
     1699msgid "Summary"
     1700msgstr ""
     1701
     1702#: includes/register-kinds.php:81
     1703#: includes/register-kinds.php:150
     1704#: includes/register-kinds.php:219
     1705#: includes/register-kinds.php:288
     1706#: includes/register-kinds.php:357
     1707#: includes/register-kinds.php:493
     1708msgid "Publication/Website"
     1709msgstr ""
     1710
     1711#: includes/register-kinds.php:85
     1712#: includes/register-kinds.php:154
     1713#: includes/register-kinds.php:223
     1714#: includes/register-kinds.php:292
     1715#: includes/register-kinds.php:361
     1716msgid "Publish Date"
     1717msgstr ""
     1718
     1719#: includes/register-kinds.php:89
     1720#: includes/register-kinds.php:158
     1721#: includes/register-kinds.php:227
     1722#: includes/register-kinds.php:296
     1723#: includes/register-kinds.php:365
     1724msgid "Updated Time"
     1725msgstr ""
     1726
     1727#: includes/register-kinds.php:93
     1728#: includes/register-kinds.php:162
     1729#: includes/register-kinds.php:231
     1730#: includes/register-kinds.php:300
     1731#: includes/register-kinds.php:369
     1732#: includes/register-kinds.php:445
     1733#: includes/register-kinds.php:505
     1734#: includes/register-kinds.php:570
     1735#: includes/register-kinds.php:635
     1736msgid "Tags"
     1737msgstr ""
     1738
     1739#: includes/register-kinds.php:100
     1740#: includes/register-kinds.php:169
     1741#: includes/register-kinds.php:238
     1742#: includes/register-kinds.php:307
     1743#: includes/register-kinds.php:376
     1744#: includes/register-kinds.php:512
     1745msgid "Featured Media"
     1746msgstr ""
     1747
     1748#: includes/register-kinds.php:109
     1749msgid "a reply to content typically on another site"
     1750msgstr ""
     1751
     1752#: includes/register-kinds.php:120
     1753#: includes/register-kinds.php:127
     1754msgid "Repost"
     1755msgstr ""
     1756
     1757#: includes/register-kinds.php:121
     1758msgid "Reposts"
     1759msgstr ""
     1760
    16001761#: includes/register-kinds.php:122
     1762msgid "Reposted"
     1763msgstr ""
     1764
     1765#: includes/register-kinds.php:178
     1766msgid "a complete reposting of content from another site"
     1767msgstr ""
     1768
     1769#: includes/register-kinds.php:189
     1770#: includes/register-kinds.php:196
     1771msgid "Like"
     1772msgstr ""
     1773
     1774#: includes/register-kinds.php:190
     1775msgid "Likes"
     1776msgstr ""
     1777
    16011778#: includes/register-kinds.php:191
     1779msgid "Liked"
     1780msgstr ""
     1781
     1782#: includes/register-kinds.php:247
     1783msgid "a way to pay compliments to the original post/poster of external content"
     1784msgstr ""
     1785
     1786#: includes/register-kinds.php:258
     1787msgid "Favorite"
     1788msgstr ""
     1789
     1790#: includes/register-kinds.php:259
     1791msgid "Favorites"
     1792msgstr ""
     1793
    16021794#: includes/register-kinds.php:260
     1795#: includes/register-kinds.php:265
     1796msgid "Favorited"
     1797msgstr ""
     1798
     1799#: includes/register-kinds.php:316
     1800msgid "special to the author"
     1801msgstr ""
     1802
     1803#: includes/register-kinds.php:327
     1804#: includes/register-kinds.php:334
     1805msgid "Bookmark"
     1806msgstr ""
     1807
     1808#: includes/register-kinds.php:328
     1809msgid "Bookmarks"
     1810msgstr ""
     1811
    16031812#: includes/register-kinds.php:329
    1604 #: includes/register-kinds.php:472
     1813msgid "Bookmarked"
     1814msgstr ""
     1815
     1816#: includes/register-kinds.php:385
     1817msgid "storing a link/bookmark for personal use or sharing with others"
     1818msgstr ""
     1819
     1820#: includes/register-kinds.php:396
     1821msgid "Photo"
     1822msgstr ""
     1823
     1824#: includes/register-kinds.php:397
     1825msgid "Photos"
     1826msgstr ""
     1827
     1828#: includes/register-kinds.php:401
     1829msgid "a post with an embedded image/photo as its primary focus"
     1830msgstr ""
     1831
     1832#: includes/register-kinds.php:412
     1833msgid "Video"
     1834msgstr ""
     1835
     1836#: includes/register-kinds.php:413
     1837msgid "Videos"
     1838msgstr ""
     1839
     1840#: includes/register-kinds.php:417
     1841msgid "a post with an embedded video as its primary focus"
     1842msgstr ""
     1843
     1844#: includes/register-kinds.php:428
     1845msgid "Audio"
     1846msgstr ""
     1847
     1848#: includes/register-kinds.php:429
     1849msgid "Audios"
     1850msgstr ""
     1851
     1852#: includes/register-kinds.php:433
     1853msgid "a post with an embedded audio file as its primary focus"
     1854msgstr ""
     1855
     1856#: includes/register-kinds.php:444
     1857msgid "Tag"
     1858msgstr ""
     1859
     1860#: includes/register-kinds.php:446
     1861msgid "Tagged"
     1862msgstr ""
     1863
     1864#: includes/register-kinds.php:449
     1865msgid "allows you to tag a post as being of a specific category or tag, or for person tagging"
     1866msgstr ""
     1867
     1868#: includes/register-kinds.php:461
     1869msgid "RSVPs"
     1870msgstr ""
     1871
     1872#: includes/register-kinds.php:462
     1873msgid "RSVPed"
     1874msgstr ""
     1875
     1876#: includes/register-kinds.php:477
     1877msgid "Event Details"
     1878msgstr ""
     1879
     1880#: includes/register-kinds.php:497
     1881msgid "Start Date"
     1882msgstr ""
     1883
     1884#: includes/register-kinds.php:501
     1885msgid "End Date"
     1886msgstr ""
     1887
     1888#: includes/register-kinds.php:521
     1889msgid "a specific type of reply regarding attendance of an event"
     1890msgstr ""
     1891
     1892#: includes/register-kinds.php:532
     1893msgid "Listen"
     1894msgstr ""
     1895
     1896#: includes/register-kinds.php:533
     1897msgid "Listens"
     1898msgstr ""
     1899
    16051900#: includes/register-kinds.php:534
     1901msgid "Listened"
     1902msgstr ""
     1903
     1904#: includes/register-kinds.php:539
     1905msgid "Listen To"
     1906msgstr ""
     1907
     1908#: includes/register-kinds.php:562
     1909msgid "Album"
     1910msgstr ""
     1911
     1912#: includes/register-kinds.php:566
     1913#: includes/register-kinds.php:631
     1914msgid "Release Date"
     1915msgstr ""
     1916
     1917#: includes/register-kinds.php:577
     1918msgid "Album Art"
     1919msgstr ""
     1920
     1921#: includes/register-kinds.php:586
     1922msgid "listening to audio; sometimes called a scrobble"
     1923msgstr ""
     1924
     1925#: includes/register-kinds.php:597
     1926msgid "Watch"
     1927msgstr ""
     1928
     1929#: includes/register-kinds.php:598
     1930msgid "Watches"
     1931msgstr ""
     1932
    16061933#: includes/register-kinds.php:599
     1934msgid "Watched"
     1935msgstr ""
     1936
     1937#: includes/register-kinds.php:604
     1938msgid "Watching To"
     1939msgstr ""
     1940
     1941#: includes/register-kinds.php:627
     1942msgid "Series"
     1943msgstr ""
     1944
     1945#: includes/register-kinds.php:642
     1946msgid "Poster"
     1947msgstr ""
     1948
     1949#: includes/register-kinds.php:651
     1950msgid "watching a movie, television show, online video, play or other visual-based event"
     1951msgstr ""
     1952
     1953#: includes/register-kinds.php:662
     1954msgid "Checkin"
     1955msgstr ""
     1956
     1957#: includes/register-kinds.php:663
     1958msgid "Checkins"
     1959msgstr ""
     1960
    16071961#: includes/register-kinds.php:664
    1608 #: templates/reply-metabox.php:42
    1609 msgid "Name"
    1610 msgstr ""
    1611 
    1612 #: includes/register-kinds.php:57
    1613 #: includes/register-kinds.php:126
    1614 #: includes/register-kinds.php:195
    1615 #: includes/register-kinds.php:264
    1616 #: includes/register-kinds.php:333
    1617 #: includes/register-kinds.php:476
    1618 #: includes/register-kinds.php:538
    1619 #: includes/register-kinds.php:603
    1620 #: includes/register-kinds.php:668
    1621 #: lib/parse-this/includes/class-rest-parse-this.php:49
    1622 #: templates/reply-metabox.php:36
    1623 msgid "URL"
    1624 msgstr ""
    1625 
    1626 #: includes/register-kinds.php:61
    1627 #: includes/register-kinds.php:130
    1628 #: includes/register-kinds.php:199
    1629 #: includes/register-kinds.php:268
    1630 #: includes/register-kinds.php:337
    1631 #: templates/reply-author.php:6
    1632 #: templates/reply-metabox.php:31
    1633 msgid "Author"
    1634 msgstr ""
    1635 
    1636 #: includes/register-kinds.php:68
    1637 #: includes/register-kinds.php:137
    1638 #: includes/register-kinds.php:206
    1639 #: includes/register-kinds.php:275
    1640 #: includes/register-kinds.php:344
    1641 #: includes/register-kinds.php:480
    1642 #: includes/register-kinds.php:549
    1643 #: includes/register-kinds.php:614
    1644 msgid "Summary"
    1645 msgstr ""
    1646 
    1647 #: includes/register-kinds.php:72
    1648 #: includes/register-kinds.php:141
    1649 #: includes/register-kinds.php:210
    1650 #: includes/register-kinds.php:279
    1651 #: includes/register-kinds.php:348
    1652 #: includes/register-kinds.php:484
    1653 msgid "Publication/Website"
    1654 msgstr ""
    1655 
    1656 #: includes/register-kinds.php:76
    1657 #: includes/register-kinds.php:145
    1658 #: includes/register-kinds.php:214
    1659 #: includes/register-kinds.php:283
    1660 #: includes/register-kinds.php:352
    1661 msgid "Publish Date"
    1662 msgstr ""
    1663 
    1664 #: includes/register-kinds.php:80
    1665 #: includes/register-kinds.php:149
    1666 #: includes/register-kinds.php:218
    1667 #: includes/register-kinds.php:287
    1668 #: includes/register-kinds.php:356
    1669 msgid "Updated Time"
    1670 msgstr ""
    1671 
    1672 #: includes/register-kinds.php:84
    1673 #: includes/register-kinds.php:153
    1674 #: includes/register-kinds.php:222
    1675 #: includes/register-kinds.php:291
    1676 #: includes/register-kinds.php:360
    1677 #: includes/register-kinds.php:436
    1678 #: includes/register-kinds.php:496
    1679 #: includes/register-kinds.php:561
    1680 #: includes/register-kinds.php:626
    1681 msgid "Tags"
    1682 msgstr ""
    1683 
    1684 #: includes/register-kinds.php:91
    1685 #: includes/register-kinds.php:160
    1686 #: includes/register-kinds.php:229
    1687 #: includes/register-kinds.php:298
    1688 #: includes/register-kinds.php:367
    1689 #: includes/register-kinds.php:503
    1690 msgid "Featured Media"
    1691 msgstr ""
    1692 
    1693 #: includes/register-kinds.php:100
    1694 msgid "a reply to content typically on another site"
    1695 msgstr ""
    1696 
    1697 #: includes/register-kinds.php:111
    1698 #: includes/register-kinds.php:118
    1699 msgid "Repost"
    1700 msgstr ""
    1701 
    1702 #: includes/register-kinds.php:112
    1703 msgid "Reposts"
    1704 msgstr ""
    1705 
    1706 #: includes/register-kinds.php:113
    1707 msgid "Reposted"
    1708 msgstr ""
    1709 
    1710 #: includes/register-kinds.php:169
    1711 msgid "a complete reposting of content from another site"
    1712 msgstr ""
    1713 
    1714 #: includes/register-kinds.php:180
    1715 #: includes/register-kinds.php:187
    1716 msgid "Like"
    1717 msgstr ""
    1718 
    1719 #: includes/register-kinds.php:181
    1720 msgid "Likes"
    1721 msgstr ""
    1722 
    1723 #: includes/register-kinds.php:182
    1724 msgid "Liked"
    1725 msgstr ""
    1726 
    1727 #: includes/register-kinds.php:238
    1728 msgid "a way to pay compliments to the original post/poster of external content"
    1729 msgstr ""
    1730 
    1731 #: includes/register-kinds.php:249
    1732 msgid "Favorite"
    1733 msgstr ""
    1734 
    1735 #: includes/register-kinds.php:250
    1736 msgid "Favorites"
    1737 msgstr ""
    1738 
    1739 #: includes/register-kinds.php:251
    1740 #: includes/register-kinds.php:256
    1741 msgid "Favorited"
    1742 msgstr ""
    1743 
    1744 #: includes/register-kinds.php:307
    1745 msgid "special to the author"
    1746 msgstr ""
    1747 
    1748 #: includes/register-kinds.php:318
    1749 #: includes/register-kinds.php:325
    1750 msgid "Bookmark"
    1751 msgstr ""
    1752 
    1753 #: includes/register-kinds.php:319
    1754 msgid "Bookmarks"
    1755 msgstr ""
    1756 
    1757 #: includes/register-kinds.php:320
    1758 msgid "Bookmarked"
    1759 msgstr ""
    1760 
    1761 #: includes/register-kinds.php:376
    1762 msgid "storing a link/bookmark for personal use or sharing with others"
    1763 msgstr ""
    1764 
    1765 #: includes/register-kinds.php:387
    1766 msgid "Photo"
    1767 msgstr ""
    1768 
    1769 #: includes/register-kinds.php:388
    1770 msgid "Photos"
    1771 msgstr ""
    1772 
    1773 #: includes/register-kinds.php:392
    1774 msgid "a post with an embedded image/photo as its primary focus"
    1775 msgstr ""
    1776 
    1777 #: includes/register-kinds.php:403
    1778 msgid "Video"
    1779 msgstr ""
    1780 
    1781 #: includes/register-kinds.php:404
    1782 msgid "Videos"
    1783 msgstr ""
    1784 
    1785 #: includes/register-kinds.php:408
    1786 msgid "a post with an embedded video as its primary focus"
    1787 msgstr ""
    1788 
    1789 #: includes/register-kinds.php:419
    1790 msgid "Audio"
    1791 msgstr ""
    1792 
    1793 #: includes/register-kinds.php:420
    1794 msgid "Audios"
    1795 msgstr ""
    1796 
    1797 #: includes/register-kinds.php:424
    1798 msgid "a post with an embedded audio file as its primary focus"
    1799 msgstr ""
    1800 
    1801 #: includes/register-kinds.php:435
    1802 msgid "Tag"
    1803 msgstr ""
    1804 
    1805 #: includes/register-kinds.php:437
    1806 msgid "Tagged"
    1807 msgstr ""
    1808 
    1809 #: includes/register-kinds.php:440
    1810 msgid "allows you to tag a post as being of a specific category or tag, or for person tagging"
    1811 msgstr ""
    1812 
    1813 #: includes/register-kinds.php:452
    1814 msgid "RSVPs"
    1815 msgstr ""
    1816 
    1817 #: includes/register-kinds.php:453
    1818 msgid "RSVPed"
    1819 msgstr ""
    1820 
    1821 #: includes/register-kinds.php:468
    1822 msgid "Event Details"
    1823 msgstr ""
    1824 
    1825 #: includes/register-kinds.php:488
    1826 msgid "Start Date"
    1827 msgstr ""
    1828 
    1829 #: includes/register-kinds.php:492
    1830 msgid "End Date"
    1831 msgstr ""
    1832 
    1833 #: includes/register-kinds.php:512
    1834 msgid "a specific type of reply regarding attendance of an event"
    1835 msgstr ""
    1836 
    1837 #: includes/register-kinds.php:523
    1838 msgid "Listen"
    1839 msgstr ""
    1840 
    1841 #: includes/register-kinds.php:524
    1842 msgid "Listens"
    1843 msgstr ""
    1844 
    1845 #: includes/register-kinds.php:525
    1846 msgid "Listened"
    1847 msgstr ""
    1848 
    1849 #: includes/register-kinds.php:530
    1850 msgid "Listen To"
    1851 msgstr ""
    1852 
    1853 #: includes/register-kinds.php:553
    1854 msgid "Album"
    1855 msgstr ""
    1856 
    1857 #: includes/register-kinds.php:557
    1858 #: includes/register-kinds.php:622
    1859 msgid "Release Date"
    1860 msgstr ""
    1861 
    1862 #: includes/register-kinds.php:568
    1863 msgid "Album Art"
    1864 msgstr ""
    1865 
    1866 #: includes/register-kinds.php:577
    1867 msgid "listening to audio; sometimes called a scrobble"
    1868 msgstr ""
    1869 
    1870 #: includes/register-kinds.php:588
    1871 msgid "Watch"
    1872 msgstr ""
    1873 
    1874 #: includes/register-kinds.php:589
    1875 msgid "Watches"
    1876 msgstr ""
    1877 
    1878 #: includes/register-kinds.php:590
    1879 msgid "Watched"
    1880 msgstr ""
    1881 
    1882 #: includes/register-kinds.php:595
    1883 msgid "Watching To"
    1884 msgstr ""
    1885 
    1886 #: includes/register-kinds.php:618
    1887 msgid "Series"
    1888 msgstr ""
    1889 
    1890 #: includes/register-kinds.php:633
    1891 msgid "Poster"
    1892 msgstr ""
    1893 
    1894 #: includes/register-kinds.php:642
    1895 msgid "watching a movie, television show, online video, play or other visual-based event"
    1896 msgstr ""
    1897 
    1898 #: includes/register-kinds.php:653
    1899 msgid "Checkin"
    1900 msgstr ""
    1901 
    1902 #: includes/register-kinds.php:654
    1903 msgid "Checkins"
    1904 msgstr ""
    1905 
    1906 #: includes/register-kinds.php:655
    19071962msgid "Checked into"
    19081963msgstr ""
    19091964
    1910 #: includes/register-kinds.php:660
     1965#: includes/register-kinds.php:669
    19111966msgid "Checked In"
    19121967msgstr ""
    19131968
    1914 #: includes/register-kinds.php:672
     1969#: includes/register-kinds.php:681
    19151970msgid "Latitude"
    19161971msgstr ""
    19171972
    1918 #: includes/register-kinds.php:680
     1973#: includes/register-kinds.php:689
    19191974msgid "Longitude"
    19201975msgstr ""
    19211976
    1922 #: includes/register-kinds.php:688
     1977#: includes/register-kinds.php:697
    19231978msgid "Street Address"
    19241979msgstr ""
    19251980
    1926 #: includes/register-kinds.php:692
     1981#: includes/register-kinds.php:701
    19271982msgid "Locality"
    19281983msgstr ""
    19291984
    1930 #: includes/register-kinds.php:696
     1985#: includes/register-kinds.php:705
    19311986msgid "Region"
    19321987msgstr ""
    19331988
    1934 #: includes/register-kinds.php:700
     1989#: includes/register-kinds.php:709
    19351990msgid "Country Name"
    19361991msgstr ""
    19371992
    1938 #: includes/register-kinds.php:704
     1993#: includes/register-kinds.php:713
    19391994msgid "Postal Code"
    19401995msgstr ""
    19411996
    1942 #: includes/register-kinds.php:710
     1997#: includes/register-kinds.php:719
    19431998msgid "identifying you are at a particular geographic location"
    19441999msgstr ""
    19452000
    1946 #: includes/register-kinds.php:721
     2001#: includes/register-kinds.php:730
    19472002msgid "Wish"
    19482003msgstr ""
    19492004
    1950 #: includes/register-kinds.php:722
     2005#: includes/register-kinds.php:731
    19512006msgid "Wishes"
    19522007msgstr ""
    19532008
    1954 #: includes/register-kinds.php:723
     2009#: includes/register-kinds.php:732
    19552010msgid "Wished"
    19562011msgstr ""
    19572012
    1958 #: includes/register-kinds.php:726
     2013#: includes/register-kinds.php:735
    19592014msgid "a post indicating a desire/wish. The archive of which would be a wishlist, such as a gift registry or similar"
    19602015msgstr ""
    19612016
    1962 #: includes/register-kinds.php:737
     2017#: includes/register-kinds.php:746
    19632018msgid "Play"
    19642019msgstr ""
    19652020
    1966 #: includes/register-kinds.php:738
     2021#: includes/register-kinds.php:747
    19672022msgid "Playing"
    19682023msgstr ""
    19692024
    1970 #: includes/register-kinds.php:739
     2025#: includes/register-kinds.php:748
    19712026msgid "Played"
    19722027msgstr ""
    19732028
    1974 #: includes/register-kinds.php:742
     2029#: includes/register-kinds.php:751
    19752030msgid "playing a game"
    19762031msgstr ""
    19772032
    1978 #: includes/register-kinds.php:753
    1979 #: includes/register-kinds.php:754
     2033#: includes/register-kinds.php:762
     2034#: includes/register-kinds.php:763
    19802035msgid "Weather"
    19812036msgstr ""
    19822037
    1983 #: includes/register-kinds.php:758
     2038#: includes/register-kinds.php:767
    19842039msgid "current weather conditions"
    19852040msgstr ""
    19862041
    1987 #: includes/register-kinds.php:769
    1988 #: includes/register-kinds.php:770
     2042#: includes/register-kinds.php:778
     2043#: includes/register-kinds.php:779
    19892044msgid "Exercise"
    19902045msgstr ""
    19912046
    1992 #: includes/register-kinds.php:771
     2047#: includes/register-kinds.php:780
    19932048msgid "Exercised"
    19942049msgstr ""
    19952050
    1996 #: includes/register-kinds.php:774
     2051#: includes/register-kinds.php:783
    19972052msgid "some form of physical activity or workout (examples: walk, run, cycle, hike, yoga, etc.)"
    19982053msgstr ""
    19992054
    2000 #: includes/register-kinds.php:785
     2055#: includes/register-kinds.php:794
    20012056msgid "Trip"
    20022057msgstr ""
    20032058
    2004 #: includes/register-kinds.php:786
     2059#: includes/register-kinds.php:795
    20052060msgid "Trips"
    20062061msgstr ""
    20072062
    2008 #: includes/register-kinds.php:787
    2009 #: includes/register-kinds.php:803
     2063#: includes/register-kinds.php:796
     2064#: includes/register-kinds.php:812
    20102065msgid "Travelled"
    20112066msgstr ""
    20122067
    2013 #: includes/register-kinds.php:790
     2068#: includes/register-kinds.php:799
    20142069msgid "represents a geographic journey"
    20152070msgstr ""
    20162071
    2017 #: includes/register-kinds.php:801
     2072#: includes/register-kinds.php:810
    20182073msgid "Itinerary"
    20192074msgstr ""
    20202075
    2021 #: includes/register-kinds.php:802
     2076#: includes/register-kinds.php:811
    20222077msgid "Itineraries"
    20232078msgstr ""
    20242079
    2025 #: includes/register-kinds.php:806
     2080#: includes/register-kinds.php:815
    20262081msgid "parts of a scheduled trip including transit by car, plane, train, etc."
    20272082msgstr ""
    20282083
    2029 #: includes/register-kinds.php:818
    2030 #: includes/register-kinds.php:819
     2084#: includes/register-kinds.php:827
     2085#: includes/register-kinds.php:828
    20312086msgid "Eat"
    20322087msgstr ""
    20332088
    2034 #: includes/register-kinds.php:820
     2089#: includes/register-kinds.php:829
    20352090msgid "Ate"
    20362091msgstr ""
    20372092
    2038 #: includes/register-kinds.php:823
     2093#: includes/register-kinds.php:832
    20392094msgid "what you are eating, perhaps for a food diary"
    20402095msgstr ""
    20412096
    2042 #: includes/register-kinds.php:835
     2097#: includes/register-kinds.php:844
    20432098msgid "Drink"
    20442099msgstr ""
    20452100
    2046 #: includes/register-kinds.php:836
     2101#: includes/register-kinds.php:845
    20472102msgid "Drinks"
    20482103msgstr ""
    20492104
    2050 #: includes/register-kinds.php:837
     2105#: includes/register-kinds.php:846
    20512106msgid "Drank"
    20522107msgstr ""
    20532108
    2054 #: includes/register-kinds.php:840
     2109#: includes/register-kinds.php:849
    20552110msgid "what you are drinking, perhaps for a food dairy"
    20562111msgstr ""
    20572112
    2058 #: includes/register-kinds.php:852
     2113#: includes/register-kinds.php:861
    20592114msgid "Follow"
    20602115msgstr ""
    20612116
    2062 #: includes/register-kinds.php:853
     2117#: includes/register-kinds.php:862
    20632118msgid "Follows"
    20642119msgstr ""
    20652120
    2066 #: includes/register-kinds.php:854
     2121#: includes/register-kinds.php:863
    20672122msgid "Followed"
    20682123msgstr ""
    20692124
    2070 #: includes/register-kinds.php:857
     2125#: includes/register-kinds.php:866
    20712126msgid "indicating you are now following or subscribing to another person`s activities online"
    20722127msgstr ""
    20732128
    2074 #: includes/register-kinds.php:869
     2129#: includes/register-kinds.php:878
    20752130msgid "Jam"
    20762131msgstr ""
    20772132
    2078 #: includes/register-kinds.php:870
     2133#: includes/register-kinds.php:879
    20792134msgid "Jams"
    20802135msgstr ""
    20812136
    2082 #: includes/register-kinds.php:871
     2137#: includes/register-kinds.php:880
    20832138msgid "Listened to"
    20842139msgstr ""
    20852140
    2086 #: includes/register-kinds.php:874
     2141#: includes/register-kinds.php:883
    20872142msgid "a particularly personally meaningful song (a listen with added emphasis)"
    20882143msgstr ""
    20892144
    2090 #: includes/register-kinds.php:886
    2091 #: includes/register-kinds.php:888
     2145#: includes/register-kinds.php:895
     2146#: includes/register-kinds.php:897
    20922147msgid "Read"
    20932148msgstr ""
    20942149
    2095 #: includes/register-kinds.php:887
     2150#: includes/register-kinds.php:896
    20962151msgid "Reads"
    20972152msgstr ""
    20982153
    2099 #: includes/register-kinds.php:891
     2154#: includes/register-kinds.php:900
    21002155msgid "reading a book, magazine, newspaper, other physical document, or online post"
    21012156msgstr ""
    21022157
    2103 #: includes/register-kinds.php:903
     2158#: includes/register-kinds.php:912
    21042159msgid "Quote"
    21052160msgstr ""
    21062161
    2107 #: includes/register-kinds.php:904
     2162#: includes/register-kinds.php:913
    21082163msgid "Quotes"
    21092164msgstr ""
    21102165
    2111 #: includes/register-kinds.php:905
     2166#: includes/register-kinds.php:914
    21122167msgid "Quoted"
    21132168msgstr ""
    21142169
    2115 #: includes/register-kinds.php:908
     2170#: includes/register-kinds.php:917
    21162171msgid "quoted content"
    21172172msgstr ""
    21182173
    2119 #: includes/register-kinds.php:920
     2174#: includes/register-kinds.php:929
    21202175msgid "Mood"
    21212176msgstr ""
    21222177
    2123 #: includes/register-kinds.php:921
     2178#: includes/register-kinds.php:930
    21242179msgid "Moods"
    21252180msgstr ""
    21262181
    2127 #: includes/register-kinds.php:922
     2182#: includes/register-kinds.php:931
    21282183msgid "Felt"
    21292184msgstr ""
    21302185
    2131 #: includes/register-kinds.php:925
     2186#: includes/register-kinds.php:934
    21322187msgid "how you are feeling (example: happy, sad, indifferent, etc.)"
    21332188msgstr ""
    21342189
    2135 #: includes/register-kinds.php:937
     2190#: includes/register-kinds.php:946
    21362191msgid "Recipe"
    21372192msgstr ""
    21382193
    2139 #: includes/register-kinds.php:938
     2194#: includes/register-kinds.php:947
    21402195msgid "Recipes"
    21412196msgstr ""
    21422197
    2143 #: includes/register-kinds.php:939
     2198#: includes/register-kinds.php:948
    21442199msgid "Cooked"
    21452200msgstr ""
    21462201
    2147 #: includes/register-kinds.php:942
     2202#: includes/register-kinds.php:951
    21482203msgid "list of ingredients and directions for making food or drink"
    21492204msgstr ""
    21502205
    2151 #: includes/register-kinds.php:954
     2206#: includes/register-kinds.php:963
    21522207msgid "Issue"
    21532208msgstr ""
    21542209
    2155 #: includes/register-kinds.php:955
     2210#: includes/register-kinds.php:964
    21562211msgid "Issues"
    21572212msgstr ""
    21582213
    2159 #: includes/register-kinds.php:956
     2214#: includes/register-kinds.php:965
    21602215msgid "Filed an Issue"
    21612216msgstr ""
    21622217
    2163 #: includes/register-kinds.php:959
     2218#: includes/register-kinds.php:968
    21642219msgid "Issue is a special kind of article post that is a reply to typically some source code, though potentially anything at a source control repository."
    21652220msgstr ""
    21662221
    2167 #: includes/register-kinds.php:971
     2222#: includes/register-kinds.php:980
    21682223msgid "Question"
    21692224msgstr ""
    21702225
    2171 #: includes/register-kinds.php:972
     2226#: includes/register-kinds.php:981
    21722227msgid "Questions"
    21732228msgstr ""
    21742229
    2175 #: includes/register-kinds.php:973
     2230#: includes/register-kinds.php:982
    21762231msgid "Asked a question"
    21772232msgstr ""
    21782233
    2179 #: includes/register-kinds.php:976
     2234#: includes/register-kinds.php:985
    21802235msgid "Question is a post type for soliciting answer replies, which are then typically up/down voted by others and then displayed underneath the question post ordered by highest positive vote count rather than time ordered."
    21812236msgstr ""
    21822237
    2183 #: includes/register-kinds.php:988
     2238#: includes/register-kinds.php:997
    21842239msgid "Sleep"
    21852240msgstr ""
    21862241
    2187 #: includes/register-kinds.php:989
     2242#: includes/register-kinds.php:998
    21882243msgid "Sleeps"
    21892244msgstr ""
    21902245
    2191 #: includes/register-kinds.php:990
     2246#: includes/register-kinds.php:999
    21922247msgid "Slept"
    21932248msgstr ""
    21942249
    2195 #: includes/register-kinds.php:993
     2250#: includes/register-kinds.php:1002
    21962251msgid "Sleep is a passive metrics post type that indicates how much time (and often a graph of how deeply) a person has slept."
    21972252msgstr ""
    21982253
    2199 #: includes/register-kinds.php:1005
     2254#: includes/register-kinds.php:1014
    22002255msgid "Event"
    22012256msgstr ""
    22022257
    2203 #: includes/register-kinds.php:1006
     2258#: includes/register-kinds.php:1015
    22042259msgid "Events"
    22052260msgstr ""
    22062261
    2207 #: includes/register-kinds.php:1007
     2262#: includes/register-kinds.php:1016
    22082263msgid "Planned"
    22092264msgstr ""
    22102265
    2211 #: includes/register-kinds.php:1010
     2266#: includes/register-kinds.php:1019
    22122267msgid "An event is a type of post that in addition to a post name (event title) has a start datetime (likely end datetime), and a location."
    22132268msgstr ""
    22142269
    2215 #: includes/register-kinds.php:1021
     2270#: includes/register-kinds.php:1030
    22162271msgid "Review"
    22172272msgstr ""
    22182273
    2219 #: includes/register-kinds.php:1022
     2274#: includes/register-kinds.php:1031
    22202275msgid "Reviews"
    22212276msgstr ""
    22222277
    2223 #: includes/register-kinds.php:1023
     2278#: includes/register-kinds.php:1032
    22242279msgid "Reviewed"
    22252280msgstr ""
    22262281
    2227 #: includes/register-kinds.php:1026
     2282#: includes/register-kinds.php:1035
    22282283msgid "A review is a post evaluating a product or service, usually involving a written description, sometimes with summary numerical evaluations, also known as just a rating."
    22292284msgstr ""
    22302285
    2231 #: includes/register-kinds.php:1038
     2286#: includes/register-kinds.php:1047
    22322287msgid "Acquisition"
    22332288msgstr ""
    22342289
    2235 #: includes/register-kinds.php:1039
     2290#: includes/register-kinds.php:1048
    22362291msgid "Acquisitions"
    22372292msgstr ""
    22382293
    2239 #: includes/register-kinds.php:1040
     2294#: includes/register-kinds.php:1049
    22402295msgid "Acquired"
    22412296msgstr ""
    22422297
    2243 #: includes/register-kinds.php:1043
     2298#: includes/register-kinds.php:1052
    22442299msgid "Purchases, gifts, found things, or objects donated"
    22452300msgstr ""
    22462301
    2247 #: includes/register-kinds.php:1054
     2302#: includes/register-kinds.php:1063
    22482303msgid "Craft"
    22492304msgstr ""
    22502305
    2251 #: includes/register-kinds.php:1055
     2306#: includes/register-kinds.php:1064
    22522307msgid "Crafts"
    22532308msgstr ""
    22542309
    2255 #: includes/register-kinds.php:1056
     2310#: includes/register-kinds.php:1065
    22562311msgid "Crafted"
    22572312msgstr ""
    22582313
    2259 #: includes/register-kinds.php:1059
     2314#: includes/register-kinds.php:1068
    22602315msgid "Activities like knitting, crocheting, cross stitch, wood working, restoration, 3d printing...the activity of building something."
    22612316msgstr ""
    22622317
    2263 #: indieweb-post-kinds.php:65
     2318#: indieweb-post-kinds.php:87
    22642319msgid "Parse This is not installed. Please advise the developer"
    22652320msgstr ""
    22662321
    2267 #: indieweb-post-kinds.php:98
     2322#. translators: %s: Name of the plugin, or path of the file, that loaded the older copy of Parse This.
     2323#: indieweb-post-kinds.php:109
     2324#, php-format
     2325msgid "Post Kinds: %s is loading a copy of Parse This older than version 2.0. Post Kinds will keep working, but URL lookups in the post editor use that older copy. Updating that plugin, or installing the Parse This plugin (version 2.0 or later), resolves this."
     2326msgstr ""
     2327
     2328#: indieweb-post-kinds.php:209
    22682329msgid "Classic Editor or Disable Gutenberg Plugin is not active. The Post Kinds plugin will not function correctly at this time without using the Classic Editor."
    22692330msgstr ""
    22702331
    2271 #: indieweb-post-kinds.php:211
     2332#: indieweb-post-kinds.php:353
    22722333msgid "Settings"
    22732334msgstr ""
    22742335
    2275 #: indieweb-post-kinds.php:233
     2336#: indieweb-post-kinds.php:384
    22762337msgid "For responses to URLs, such as responding to a post or article, this site allows the storage of data around the post/article in order to generate a rich citation. Items such as author name and image, summary of the text, embed provided by third-party site, etc may be stored and are solely to provide this context. We will remove any of this on request."
    22772338msgstr ""
    22782339
    2279 #: lib/parse-this/includes/class-parse-this-discovery.php:57
    2280 #: lib/parse-this/includes/class-parse-this-opml.php:17
    2281 #: lib/parse-this/includes/class-parse-this.php:231
    2282 #: lib/parse-this/includes/class-parse-this.php:265
    2283 msgid "A valid URL was not provided."
    2284 msgstr ""
    2285 
    2286 #: lib/parse-this/includes/class-rest-parse-this.php:19
    2287 #: lib/parse-this/includes/class-rest-parse-this.php:20
    2288 msgid "Parse This"
    2289 msgstr ""
    2290 
    2291 #: lib/parse-this/includes/class-rest-parse-this.php:35
    2292 msgid "Parse This Debugger"
    2293 msgstr ""
    2294 
    2295 #: lib/parse-this/includes/class-rest-parse-this.php:55
    2296 msgid "MF2"
    2297 msgstr ""
    2298 
    2299 #: lib/parse-this/includes/class-rest-parse-this.php:63
    2300 msgid "Feed Discovery"
    2301 msgstr ""
    2302 
    2303 #: lib/parse-this/includes/class-rest-parse-this.php:71
    2304 msgid "References"
    2305 msgstr ""
    2306 
    2307 #: lib/parse-this/includes/class-rest-parse-this.php:79
    2308 msgid "Clean up Location"
    2309 msgstr ""
    2310 
    2311 #: lib/parse-this/includes/class-rest-parse-this.php:87
    2312 msgid "Return Type"
    2313 msgstr ""
    2314 
    2315 #: lib/parse-this/includes/class-rest-parse-this.php:91
    2316 msgid "Single"
    2317 msgstr ""
    2318 
    2319 #: lib/parse-this/includes/class-rest-parse-this.php:92
    2320 msgid "Feed"
    2321 msgstr ""
    2322 
    2323 #: lib/parse-this/includes/class-rest-parse-this.php:98
    2324 msgid "Follow Author Links"
    2325 msgstr ""
    2326 
    2327 #: lib/parse-this/includes/class-rest-parse-this.php:107
    2328 msgid "Parse"
    2329 msgstr ""
    2330 
    2331 #: templates/reply-author.php:2
     2340#: templates/reply-author.php:16
    23322341msgid "Information on the Author or Artist of the Piece"
    23332342msgstr ""
    23342343
    2335 #: templates/reply-author.php:3
     2344#: templates/reply-author.php:17
    23362345msgid "(Multiple Entries separated by semicolon)"
    23372346msgstr ""
    23382347
    2339 #: templates/reply-author.php:18
     2348#: templates/reply-author.php:32
    23402349msgid "Author Photo URL"
    23412350msgstr ""
    23422351
    2343 #: templates/reply-details.php:2
     2352#: templates/reply-details.php:17
    23442353msgid "Information on what you are responding to"
    23452354msgstr ""
    23462355
    2347 #: templates/reply-details.php:4
     2356#: templates/reply-details.php:19
    23482357msgid "Summary/Quote/Caption"
    23492358msgstr ""
    23502359
    2351 #: templates/reply-details.php:9
     2360#: templates/reply-details.php:24
    23522361msgid "Site Name/Publication/Album"
    23532362msgstr ""
    23542363
    2355 #: templates/reply-details.php:14
     2364#: templates/reply-details.php:29
    23562365msgid "Published/Released"
    23572366msgstr ""
    23582367
    2359 #: templates/reply-details.php:15
     2368#: templates/reply-details.php:30
    23602369msgid "Updated"
    23612370msgstr ""
    23622371
    2363 #: templates/reply-details.php:18
     2372#: templates/reply-details.php:33
    23642373msgid "Tags (semicolon separated)"
    23652374msgstr ""
    23662375
    2367 #: templates/reply-details.php:23
     2376#: templates/reply-details.php:38
    23682377msgid "Featured Image"
    23692378msgstr ""
    23702379
    2371 #: templates/reply-metabox.php:30
     2380#: templates/reply-metabox.php:44
    23722381msgid "Details"
    23732382msgstr ""
    23742383
    2375 #: templates/reply-metabox.php:33
     2384#: templates/reply-metabox.php:46
     2385msgid "Upload or Attach Media"
     2386msgstr ""
     2387
     2388#: templates/reply-metabox.php:47
    23762389msgid "Clear"
    23772390msgstr ""
    23782391
    2379 #: templates/reply-time.php:4
     2392#: templates/reply-time.php:18
    23802393msgid "Duration"
    23812394msgstr ""
    23822395
    2383 #: templates/reply-time.php:7
     2396#: templates/reply-time.php:21
    23842397msgid "Years"
    23852398msgstr ""
    23862399
    2387 #: templates/reply-time.php:11
     2400#: templates/reply-time.php:25
    23882401msgid "Months"
    23892402msgstr ""
    23902403
    2391 #: templates/reply-time.php:15
     2404#: templates/reply-time.php:29
    23922405msgid "Days"
    23932406msgstr ""
    23942407
    2395 #: templates/reply-time.php:21
     2408#: templates/reply-time.php:35
    23962409msgid "Hours"
    23972410msgstr ""
    23982411
    2399 #: templates/reply-time.php:25
     2412#: templates/reply-time.php:39
    24002413msgid "Minutes"
    24012414msgstr ""
    24022415
    2403 #: templates/reply-time.php:29
     2416#: templates/reply-time.php:43
    24042417msgid "Seconds"
    24052418msgstr ""
    24062419
    2407 #: templates/reply-time.php:33
     2420#: templates/reply-time.php:47
    24082421msgid "Start Time and End Time will be Used to Calculate Duration if Duration Not Set."
    24092422msgstr ""
    24102423
    2411 #: templates/reply-time.php:35
     2424#: templates/reply-time.php:49
    24122425msgid "Start Time"
    24132426msgstr ""
    24142427
    2415 #: templates/reply-time.php:36
     2428#: templates/reply-time.php:50
    24162429msgid "End Time"
    24172430msgstr ""
    24182431
    2419 #: views/kind-bookmark.php:25
    2420 #: views/kind-favorite.php:24
    2421 #: views/kind-issue.php:24
    2422 #: views/kind-jam.php:28
    2423 #: views/kind-like.php:25
    2424 #: views/kind-listen.php:30
    2425 #: views/kind-read.php:28
    2426 #: views/kind-video.php:45
    2427 #: views/kind-watch.php:29
    2428 #: views/kind.php:35
     2432#: views/kind-bookmark.php:39
     2433#: views/kind-favorite.php:38
     2434#: views/kind-issue.php:38
     2435#: views/kind-jam.php:46
     2436#: views/kind-like.php:39
     2437#: views/kind-listen.php:48
     2438#: views/kind-read.php:42
     2439#: views/kind-video.php:63
     2440#: views/kind-watch.php:47
     2441#: views/kind.php:53
    24292442msgid "by"
    24302443msgstr ""
    24312444
    2432 #: views/kind-event.php:22
     2445#: views/kind-event.php:39
    24332446msgid "Start"
    24342447msgstr ""
    24352448
    2436 #: views/kind-event.php:25
     2449#: views/kind-event.php:42
    24372450msgid "End"
    24382451msgstr ""
    24392452
    2440 #: views/kind-itinerary.php:33
     2453#: views/kind-itinerary.php:56
    24412454msgid "Departs: "
    24422455msgstr ""
    24432456
    2444 #: views/kind-itinerary.php:38
     2457#: views/kind-itinerary.php:61
    24452458msgid "Arrives: "
    24462459msgstr ""
    24472460
    2448 #: views/kind-jam.php:31
    2449 #: views/kind-listen.php:33
    2450 #: views/kind-watch.php:32
     2461#: views/kind-jam.php:49
     2462#: views/kind-listen.php:51
     2463#: views/kind-watch.php:50
    24512464msgid " from "
    24522465msgstr ""
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/includes/autoload.php

    r2332001 r3738687  
    11<?php
     2/**
     3 * Class autoloader for Parse This.
     4 *
     5 * @package Parse_This
     6 */
    27
    3 // autoloader for Parse This
     8defined( 'ABSPATH' ) || exit;
     9
     10/*
     11 * Maps classes in the ParseThis namespace to files in this directory, for
     12 * example ParseThis\MF2_Utils to class-mf2-utils.php.
     13 */
    414spl_autoload_register(
    515        function ( $class ) {
    6                 $base_dir = trailingslashit( __DIR__ );
    7                 $bases    = array( 'Parse_This' );
    8                 foreach ( $bases as $base ) {
    9                         if ( strncmp( $class, $base, strlen( $base ) ) === 0 ) {
    10                                 $filename = 'class-' . strtolower( str_replace( '_', '-', $class ) );
    11                                 $file     = $base_dir . $filename . '.php';
    12                                 if ( file_exists( $file ) ) {
    13                                         require $file;
    14                                 }
    15                         }
     16                $prefix = 'ParseThis\\';
     17                if ( 0 !== strncmp( $class, $prefix, strlen( $prefix ) ) ) {
     18                        return;
     19                }
     20                $name = substr( $class, strlen( $prefix ) );
     21                $file = __DIR__ . '/class-' . strtolower( str_replace( '_', '-', $name ) ) . '.php';
     22                if ( file_exists( $file ) ) {
     23                        require $file;
    1624                }
    1725        }
    1826);
     27
     28// Deprecated global names still used by other plugins.
     29require_once __DIR__ . '/aliases.php';
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/includes/functions.php

    r2757240 r3738687  
    11<?php
    2 // Parse This Global Functions
    3 
    4 
    5 if ( ! function_exists( 'jf2_to_mf2' ) ) {
     2/**
     3 * Helper functions.
     4 *
     5 * All functions are in the ParseThis namespace. Each is wrapped in
     6 * function_exists() because several plugins bundle their own copy of Parse
     7 * This; the first copy loaded wins. Deprecated global names for some of them
     8 * are defined in aliases.php.
     9 *
     10 * @package Parse_This
     11 */
     12
     13namespace ParseThis;
     14
     15defined( 'ABSPATH' ) || exit;
     16
     17if ( ! function_exists( __NAMESPACE__ . '\\jf2_to_mf2' ) ) {
     18        /**
     19         * Converts jf2 into microformats2 JSON.
     20         *
     21         * The inverse of mf2_to_jf2(). Empty values and the _raw key are dropped.
     22         *
     23         * @since 1.0.0
     24         *
     25         * @link https://jf2.spec.indieweb.org/
     26         *
     27         * @param array|mixed $jf2 jf2 object, or array( 'items' => ... ) for a list.
     28         * @return array|mixed mf2 object with type and properties. Values that are not
     29         *                     jf2 objects are returned unchanged.
     30         */
    631        function jf2_to_mf2( $jf2 ) {
    732                if ( ! $jf2 || ! is_array( $jf2 ) ) {
    … …  
    1035                if ( 1 === count( $jf2 ) && array_key_exists( 'items', $jf2 ) ) {
    1136                        return array(
    12                                 'items' => array_map( 'jf2_to_mf2', $jf2['items'] ),
     37                                'items' => array_map( __NAMESPACE__ . '\\jf2_to_mf2', $jf2['items'] ),
    1338                        );
    1439                }
    … …  
    2449                }
    2550                if ( array_key_exists( 'children', $jf2 ) ) {
    26                         $mf2['children'] = array_map( 'jf2_to_mf2', $jf2['children'] );
     51                        $mf2['children'] = array_map( __NAMESPACE__ . '\\jf2_to_mf2', $jf2['children'] );
    2752                        unset( $jf2['children'] );
    2853                }
    … …  
    3156
    3257                foreach ( $jf2 as $key => $value ) {
    33                         // Exclude values
     58                        // Exclude values.
    3459                        if ( empty( $value ) || ( '_raw' === $key ) ) {
    3560                                continue;
    … …  
    3863                                $value = array( jf2_to_mf2( $value ) );
    3964                        } elseif ( wp_is_numeric_array( $value ) ) {
    40                                 $value = array_map( 'jf2_to_mf2', $value );
     65                                $value = array_map( __NAMESPACE__ . '\\jf2_to_mf2', $value );
    4166                        } elseif ( ! wp_is_numeric_array( $value ) ) {
    4267                                $value = array( $value );
    … …  
    4873}
    4974
    50 if ( ! function_exists( 'mf2_to_jf2' ) ) {
    51 
     75if ( ! function_exists( __NAMESPACE__ . '\\mf2_to_jf2' ) ) {
     76
     77        /**
     78         * Converts microformats2 JSON into jf2.
     79         *
     80         * Single-value property arrays are collapsed to the value, the h- prefix is
     81         * removed from types, and nested microformats are converted recursively.
     82         *
     83         * @since 1.0.0
     84         *
     85         * @link https://jf2.spec.indieweb.org/
     86         *
     87         * @param array|mixed $mf2 mf2 object, a parsed mf2 document with items, or a
     88         *                         list of mf2 objects.
     89         * @return array|mixed jf2. A list of one is returned as the single item.
     90         *                     Strings and objects are returned unchanged.
     91         */
    5292        function mf2_to_jf2( $mf2 ) {
    5393                if ( empty( $mf2 ) || is_string( $mf2 ) || is_object( $mf2 ) ) {
    … …  
    5797                $jf2 = array();
    5898
    59                 // If it is a numeric array, run this function through each item
     99                // If it is a numeric array, run this function through each item.
    60100                if ( wp_is_numeric_array( $mf2 ) ) {
    61                         $jf2 = array_map( 'mf2_to_jf2', $mf2 );
     101                        $jf2 = array_map( __NAMESPACE__ . '\\mf2_to_jf2', $mf2 );
    62102                        if ( 1 === count( $jf2 ) ) {
    63103                                return array_pop( $jf2 );
    … …  
    67107
    68108                if ( isset( $mf2['items'] ) ) {
    69                         $jf2['items'] = array_map( 'mf2_to_jf2', $mf2['items'] );
     109                        $jf2['items'] = array_map( __NAMESPACE__ . '\\mf2_to_jf2', $mf2['items'] );
    70110                }
    71111
    72112                if ( isset( $mf2['children'] ) ) {
    73                         $jf2['children'] = array_map( 'mf2_to_jf2', $mf2['children'] );
     113                        $jf2['children'] = array_map( __NAMESPACE__ . '\\mf2_to_jf2', $mf2['children'] );
    74114                }
    75115
    … …  
    82122                                if ( is_array( $value ) ) {
    83123                                        if ( wp_is_numeric_array( $value ) ) {
    84                                                 $value = array_map( 'mf2_to_jf2', $value );
     124                                                $value = array_map( __NAMESPACE__ . '\\mf2_to_jf2', $value );
    85125                                                if ( is_countable( $value ) && 1 === count( $value ) ) {
    86126                                                        $value = array_pop( $value );
    … …  
    98138
    99139
    100 if ( ! function_exists( 'jf2_location' ) ) {
    101         /*
    102          Flatten nested location properties.
    103         */
     140if ( ! function_exists( __NAMESPACE__ . '\\jf2_location' ) ) {
     141        /**
     142         * Flattens a nested jf2 location.
     143         *
     144         * The latitude, longitude and altitude are copied onto the object, and location
     145         * becomes the place's label or name (or is removed). Any check-in on the
     146         * object also receives the location's properties.
     147         *
     148         * @since 1.0.1
     149         *
     150         * @param array $data jf2 object.
     151         * @return array The updated object.
     152         */
    104153        function jf2_location( $data ) {
    105154                if ( ! array_key_exists( 'location', $data ) ) {
    106155                        return $data;
    107156                }
    108                 $location = $data['location'];
    109                 if ( is_string( $location ) ) {
     157                // jf2_references() may have moved the location or check-in to refs,
     158                // leaving its URL (as a one-item list) in its place.
     159                $from_refs = function ( $value ) use ( $data ) {
     160                        if ( is_array( $value ) && wp_is_numeric_array( $value ) && 1 === count( $value ) ) {
     161                                $value = $value[0];
     162                        }
     163                        if ( is_string( $value ) && isset( $data['refs'][ $value ] ) && is_array( $data['refs'][ $value ] ) ) {
     164                                return $data['refs'][ $value ];
     165                        }
     166                        return $value;
     167                };
     168                $location  = $from_refs( $data['location'] );
     169                if ( ! is_array( $location ) || wp_is_numeric_array( $location ) ) {
    110170                        return $data;
    111171                }
    … …  
    122182                        unset( $data['location'] );
    123183                }
     184                if ( isset( $data['checkin'] ) ) {
     185                        $data['checkin'] = $from_refs( $data['checkin'] );
     186                }
    124187                if ( array_key_exists( 'checkin', $data ) && is_array( $data['checkin'] ) ) {
    125188                        foreach ( $location as $key => $value ) {
    … …  
    134197
    135198
    136 if ( ! function_exists( 'jf2_references' ) ) {
    137         /*
    138          Turns nested properties into references per the jf2 spec
    139         */
     199if ( ! function_exists( __NAMESPACE__ . '\\jf2_author_to_card' ) ) {
     200        /**
     201         * Converts an author value into a jf2 card.
     202         *
     203         * Microsub requires author to always be a card, and jf2 consumers generally
     204         * expect one. A URL string becomes a card with that url, any other string a
     205         * card with that name, and an object without a type gets type card. A list of
     206         * authors is converted item by item; a list of one becomes that card.
     207         *
     208         * @since 2.0.0
     209         *
     210         * @param mixed $author Author: a string, an object or a list of either.
     211         * @return array|null The card (or list of cards), or null if there is no author.
     212         */
     213        function jf2_author_to_card( $author ) {
     214                if ( is_string( $author ) ) {
     215                        $author = trim( $author );
     216                        if ( '' === $author ) {
     217                                return null;
     218                        }
     219                        $key = ( preg_match( '#^https?://#i', $author ) && filter_var( $author, FILTER_VALIDATE_URL ) ) ? 'url' : 'name';
     220                        return array(
     221                                'type' => 'card',
     222                                $key   => $author,
     223                        );
     224                }
     225                if ( ! is_array( $author ) || empty( $author ) ) {
     226                        return null;
     227                }
     228                if ( wp_is_numeric_array( $author ) ) {
     229                        $cards = array_values( array_filter( array_map( __NAMESPACE__ . '\\jf2_author_to_card', $author ) ) );
     230                        if ( empty( $cards ) ) {
     231                                return null;
     232                        }
     233                        return ( 1 === count( $cards ) ) ? $cards[0] : $cards;
     234                }
     235                if ( ! isset( $author['type'] ) ) {
     236                        $author = array( 'type' => 'card' ) + $author;
     237                }
     238                return $author;
     239        }
     240}
     241
     242if ( ! function_exists( __NAMESPACE__ . '\\jf2_references' ) ) {
     243        /**
     244         * Moves nested objects into refs, per the jf2 spec.
     245         *
     246         * Any nested typed object with an http or https URL (an h-cite, h-card,
     247         * h-event, h-product or any other type) is replaced by its URL, and stored
     248         * in refs keyed by that URL, so the result is one level deep. The property
     249         * becomes a list of values, as before. Objects without such a URL stay in place, authors stay
     250         * cards, and a feed's items are left alone. References that a nested object
     251         * carries are merged into the same refs.
     252         *
     253         * @since 1.0.0
     254         * @since 2.0.0 Moves any typed object with a URL, not only citations, and
     255         *              merges nested refs.
     256         *
     257         * @link https://jf2.spec.indieweb.org/#references
     258         *
     259         * @param array $data jf2 object.
     260         * @return array The updated object.
     261         */
    140262        function jf2_references( $data ) {
     263                if ( ! is_array( $data ) ) {
     264                        return $data;
     265                }
     266                // Authors stay cards, and a feed's entries are not references.
     267                $skip = array( 'refs', 'author', 'items', 'children' );
    141268                foreach ( $data as $key => $val ) {
    142                         if ( ! is_array( $val ) ) {
     269                        if ( ! is_array( $val ) || in_array( $key, $skip, true ) || ( is_string( $key ) && '_' === substr( $key, 0, 1 ) ) ) {
    143270                                continue;
    144271                        }
    145                         if ( ! wp_is_numeric_array( $val ) ) {
    146                                 $val = array( $val );
    147                         }
    148                         if ( wp_is_numeric_array( $val ) ) {
    149                                 foreach ( $val as $value ) {
    150                                         // Indicates nested type
    151                                         if ( is_array( $value ) && array_key_exists( 'type', $value ) && 'cite' === $value['type'] ) {
    152                                                 if ( ! isset( $data['refs'] ) ) {
    153                                                         $data['refs'] = array();
    154                                                 }
    155                                                 if ( isset( $value['url'] ) ) {
    156                                                         $data['refs'][ $value['url'] ] = $value;
    157                                                         $data[ $key ]                  = array( $value['url'] );
    158                                                 }
     272                        $values  = wp_is_numeric_array( $val ) ? $val : array( $val );
     273                        $changed = false;
     274                        foreach ( $values as $i => $value ) {
     275                                if ( ! is_array( $value ) ) {
     276                                        continue;
     277                                }
     278                                // A nested object may carry references of its own: keep one flat refs.
     279                                if ( isset( $value['refs'] ) && is_array( $value['refs'] ) ) {
     280                                        foreach ( $value['refs'] as $ref_url => $ref ) {
     281                                                $data['refs'][ $ref_url ] = isset( $data['refs'][ $ref_url ] ) ? $data['refs'][ $ref_url ] + $ref : $ref;
    159282                                        }
    160                                         if ( 'category' === $key ) {
    161                                                 foreach ( $value as $k => $v ) {
    162                                                         if ( is_array( $v ) && array_key_exists( 'type', $v ) ) {
    163                                                                 if ( ! isset( $data['refs'] ) ) {
    164                                                                         $data['refs'] = array();
    165                                                                 }
    166                                                                 if ( isset( $v['url'] ) ) {
    167                                                                         $data['refs'][ $v['url'] ] = $v;
    168                                                                         $data['category'][ $k ]    = $v['url'];
    169                                                                 }
    170                                                         }
    171                                                 }
    172                                         }
    173                                 }
     283                                        unset( $value['refs'] );
     284                                        $values[ $i ] = $value;
     285                                        $changed      = true;
     286                                }
     287                                $url = $value['url'] ?? null;
     288                                if ( is_array( $url ) ) {
     289                                        $url = reset( $url );
     290                                }
     291                                // Any typed object with an http(s) URL is a reference. One without
     292                                // stays in place, where sanitizing can drop a bad URL and keep the rest.
     293                                if ( empty( $value['type'] ) || ! is_string( $value['type'] ) || ! is_string( $url ) || ! preg_match( '#^https?://#i', trim( $url ) ) ) {
     294                                        continue;
     295                                }
     296                                $data['refs'][ $url ] = isset( $data['refs'][ $url ] ) ? $data['refs'][ $url ] + $value : $value;
     297                                $values[ $i ]         = $url;
     298                                $changed              = true;
     299                        }
     300                        if ( $changed ) {
     301                                $data[ $key ] = $values;
    174302                        }
    175303                }
    … …  
    178306}
    179307
    180 if ( ! function_exists( 'url_to_author' ) ) {
    181         /**
    182          * Examine a url and try to determine the author ID it represents.
    183          *
    184          * @param string $url Permalink to check.
    185          *
    186          * @return WP_User, or null on failure.
     308if ( ! function_exists( __NAMESPACE__ . '\\url_to_author' ) ) {
     309        /**
     310         * Returns the local user that an author archive URL belongs to.
     311         *
     312         * Handles ?author=N URLs and pretty author permalinks.
     313         *
     314         * @since 1.0.0
     315         *
     316         * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
     317         *
     318         * @param string $url URL to check.
     319         * @return WP_User|false|null The user, false if no user has that ID or slug, or
     320         *                            null if the URL is not on this site or not an
     321         *                            author URL.
    187322         */
    188323        function url_to_author( $url ) {
    189324                global $wp_rewrite;
    190                 // check if url hase the same host
     325                // check if url hase the same host.
    191326                if ( wp_parse_url( site_url(), PHP_URL_HOST ) !== wp_parse_url( $url, PHP_URL_HOST ) ) {
    192327                        return null;
    193328                }
    194                 // first, check to see if there is a 'author=N' to match against
     329                // first, check to see if there is a 'author=N' to match against.
    195330                if ( preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
    196331                        $id = absint( $values[1] );
    … …  
    199334                        }
    200335                }
    201                 // check to see if we are using rewrite rules
     336                // check to see if we are using rewrite rules.
    202337                $rewrite = $wp_rewrite->wp_rewrite_rules();
    203                 // not using rewrite rules, and 'author=N' method failed, so we're out of options
     338                // not using rewrite rules, and 'author=N' method failed, so we're out of options.
    204339                if ( empty( $rewrite ) ) {
    205340                        return null;
    206341                }
    207                 // generate rewrite rule for the author url
     342                // generate rewrite rule for the author url.
    208343                $author_rewrite = $wp_rewrite->get_author_permastruct();
    209344                $author_regexp  = str_replace( '%author%', '', $author_rewrite );
    210                 // match the rewrite rule with the passed url
     345                // match the rewrite rule with the passed url.
    211346                if ( preg_match( '/https?:\/\/(.+)' . preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) {
    212347                        $user = get_user_by( 'slug', $match[2] );
    … …  
    219354}
    220355
    221 if ( ! function_exists( 'url_to_user' ) ) {
    222         /**
    223          * Get the user associated with a URL.
    224          *
    225          * @param string $url url to match
    226          * @return WP_User $user Associated user, or null if no associated user
     356if ( ! function_exists( __NAMESPACE__ . '\\url_to_user' ) ) {
     357        /**
     358         * Returns the local user associated with a URL.
     359         *
     360         * The site's home URL maps to the IndieWeb plugin's default author when the
     361         * IndieWeb plugin is active, otherwise to the only author if there is just
     362         * one. Author archive URLs map to their author, and other URLs are matched
     363         * against users' website field.
     364         *
     365         * @since 1.0.0
     366         *
     367         * @param string $url URL to check.
     368         * @return WP_User|false|null The user, or null (or false) if none matches.
    227369         */
    228370        function url_to_user( $url ) {
    … …  
    230372                        return null;
    231373                }
    232                 // Ensure has trailing slash
     374                // Ensure has trailing slash.
    233375                $url = trailingslashit( $url );
    234376                if ( ( 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ) ) && ( wp_parse_url( home_url(), PHP_URL_HOST ) === wp_parse_url( $url, PHP_URL_HOST ) ) ) {
    235377                        $url = set_url_scheme( $url, 'https' );
    236378                }
    237                 // Try to save the expense of a search query if the URL is the site URL
     379                // Try to save the expense of a search query if the URL is the site URL.
    238380                if ( home_url( '/' ) === $url ) {
    239                         // Use the Indieweb settings to set the default author
     381                        // Use the Indieweb settings to set the default author.
    240382                        if ( class_exists( 'Indieweb_Plugin' ) && ( get_option( 'iw_single_author' ) || ! is_multi_author() ) ) {
    241383                                return get_user_by( 'id', get_option( 'iw_default_author' ) );
    242384                        }
    243                         $users = get_users( array( 'who' => 'authors' ) );
     385                        $users = get_users( array( 'capability' => array( 'edit_posts' ) ) );
    244386                        if ( 1 === count( $users ) ) {
    245387                                return $users[0];
    … …  
    247389                        return null;
    248390                }
    249                 // Check if this is a author post URL
     391                // Check if this is a author post URL.
    250392                $user = url_to_author( $url );
    251                 if ( $user instanceof WP_User ) {
     393                if ( $user instanceof \WP_User ) {
    252394                        return $user;
    253395                }
    … …  
    257399                );
    258400                $users = get_users( $args );
    259                 // check result
     401                // check result.
    260402                if ( ! empty( $users ) ) {
    261403                        return $users[0];
    … …  
    265407}
    266408
    267 if ( ! function_exists( 'ifset' ) ) {
    268                 /**
    269                  * If set, return otherwise false.
    270                  *
    271                  * @param type $var Check if set.
    272                  * @return $var|false Return either $var or $return.
    273                  */
    274         function ifset( &$var, $return = false ) {
    275 
    276                         return isset( $var ) ? $var : $return;
    277         }
    278 }
    279 
    280 
    281 /*
    282  Inverse of wp_parse_url
    283  *
    284  * Slightly modified from p3k-utils (https://github.com/aaronpk/p3k-utils)
    285  * Copyright 2017 Aaron Parecki, used with permission under MIT License
    286  *
    287  * @link http://php.net/parse_url
    288  * @param  string $parsed_url the parsed URL (wp_parse_url)
    289  * @return string             the final URL
    290  */
    291 if ( ! function_exists( 'build_url' ) ) {
     409if ( ! function_exists( __NAMESPACE__ . '\\build_url' ) ) {
     410        /**
     411         * Builds a URL from its parts; the inverse of wp_parse_url().
     412         *
     413         * Slightly modified from p3k-utils. Copyright 2017 Aaron Parecki, used with
     414         * permission under the MIT License.
     415         *
     416         * @since 1.0.0
     417         *
     418         * @link https://github.com/aaronpk/p3k-utils
     419         * @link https://www.php.net/parse_url
     420         *
     421         * @param array $parsed_url URL components as returned by wp_parse_url().
     422         * @return string The URL.
     423         */
    292424        function build_url( $parsed_url ) {
    293425                        $scheme   = ! empty( $parsed_url['scheme'] ) ? $parsed_url['scheme'] . '://' : '';
    … …  
    306438
    307439
    308 if ( ! function_exists( 'normalize_url' ) ) {
    309         // Adds slash if no path is in the URL, and convert hostname to lowercase
     440if ( ! function_exists( __NAMESPACE__ . '\\normalize_url' ) ) {
     441        /**
     442         * Normalizes a URL for comparison.
     443         *
     444         * Lowercases the host and adds a / path if there is none.
     445         *
     446         * @since 1.0.0
     447         *
     448         * @param string $url   URL to normalize.
     449         * @param bool   $strip Optional. Whether to remove the query string. Default false.
     450         * @return string|null The normalized URL, or null if it has no host.
     451         */
    310452        function normalize_url( $url, $strip = false ) {
    311453                $parts = wp_parse_url( $url );
    … …  
    323465}
    324466
    325 if ( ! function_exists( 'normalize_iso8601' ) ) {
    326         // Tries to normalizes dates to a standard iso8601 string
     467if ( ! function_exists( __NAMESPACE__ . '\\normalize_iso8601' ) ) {
     468        /**
     469         * Normalizes a date string to W3C (ISO 8601) format.
     470         *
     471         * @since 1.0.0
     472         *
     473         * @param string|mixed $string Date string.
     474         * @return string|null The date in W3C format, the original string if it cannot
     475         *                     be parsed, or null if it is empty or not a string.
     476         */
    327477        function normalize_iso8601( $string ) {
    328                 $date = new DateTime( $string );
    329                 if ( $date ) {
    330                         return $date->format( DATE_W3C );
    331                 }
    332                 return $string;
    333         }
    334 }
    335 
    336 if ( ! function_exists( 'post_type_discovery' ) ) {
     478                if ( empty( $string ) || ! is_string( $string ) ) {
     479                        return null;
     480                }
     481                try {
     482                        $date = new \DateTime( $string );
     483                } catch ( \Exception $e ) {
     484                        return $string;
     485                }
     486                return $date->format( DATE_W3C );
     487        }
     488}
     489
     490if ( ! function_exists( __NAMESPACE__ . '\\pt_find_fragment_element' ) ) {
     491        /**
     492         * Finds the element a URL fragment points to.
     493         *
     494         * @since 2.0.0
     495         *
     496         * @param DOMDocument $doc Parsed page.
     497         * @param string      $id  Fragment, without the #.
     498         * @return DOMElement|null The element with that id, or null.
     499         */
     500        function pt_find_fragment_element( $doc, $id ) {
     501                if ( ! $doc instanceof \DOMDocument || ! is_string( $id ) || '' === $id ) {
     502                        return null;
     503                }
     504                $id = rawurldecode( $id );
     505                foreach ( $doc->getElementsByTagName( '*' ) as $element ) {
     506                        if ( $element->getAttribute( 'id' ) === $id ) {
     507                                return $element;
     508                        }
     509                }
     510                return null;
     511        }
     512}
     513
     514if ( ! function_exists( __NAMESPACE__ . '\\pt_effective_url' ) ) {
     515        /**
     516         * Returns the URL a response finally came from, after any redirects.
     517         *
     518         * A fragment on the requested URL (which is never sent to the server) is
     519         * kept, so that it can still pick out part of the page.
     520         *
     521         * @since 2.0.0
     522         *
     523         * @param string $url      Requested URL.
     524         * @param array  $response Response from pt_remote_get().
     525         * @return string The final URL, or $url if it isn't known.
     526         */
     527        function pt_effective_url( $url, $response ) {
     528                $object = ( is_array( $response ) && isset( $response['http_response'] ) && is_object( $response['http_response'] ) && method_exists( $response['http_response'], 'get_response_object' ) ) ? $response['http_response']->get_response_object() : null;
     529                if ( ! $object || empty( $object->url ) || ! is_string( $object->url ) || ! wp_http_validate_url( $object->url ) ) {
     530                        return $url;
     531                }
     532                $final    = $object->url;
     533                $fragment = wp_parse_url( $url, PHP_URL_FRAGMENT );
     534                if ( $fragment && ! wp_parse_url( $final, PHP_URL_FRAGMENT ) ) {
     535                        $final .= '#' . $fragment;
     536                }
     537                return $final;
     538        }
     539}
     540
     541if ( ! function_exists( __NAMESPACE__ . '\\pt_sniff_content_type' ) ) {
     542        /**
     543         * Works out a response's real format when its content type is missing,
     544         * generic or wrong.
     545         *
     546         * Feeds are often served as text/plain or text/html, and JSON as
     547         * text/plain or application/octet-stream. A body whose root element is
     548         * <rss>, <rdf:RDF> or <feed> is a feed whatever the header says (an HTML
     549         * page can't have those roots). A body that is a JSON object is checked
     550         * for JSON Feed and mf2 JSON when the header is missing or generic.
     551         *
     552         * @since 2.0.0
     553         *
     554         * @param string $content_type Content type from the response, without
     555         *                             parameters.
     556         * @param string $body         Response body.
     557         * @return string The content type to use: application/rss+xml,
     558         *                application/atom+xml, application/feed+json or
     559         *                application/mf2+json when sniffed, otherwise
     560         *                $content_type unchanged.
     561         */
     562        function pt_sniff_content_type( $content_type, $body ) {
     563                if ( ! is_string( $body ) || '' === $body ) {
     564                        return $content_type;
     565                }
     566                $known = array( 'application/rss+xml', 'application/atom+xml', 'application/feed+json', 'application/mf2+json', 'application/jf2+json', 'application/jf2feed+json' );
     567                if ( in_array( $content_type, $known, true ) ) {
     568                        return $content_type;
     569                }
     570                // Skip a byte order mark, the XML declaration, processing instructions, comments and a doctype.
     571                $start = ltrim( substr( $body, 0, 4096 ), "\xEF\xBB\xBF \t\r\n" );
     572                $start = preg_replace( '/^(\s*(<\?[^>]*\?>|<!--.*?-->|<!DOCTYPE[^>]*>))+\s*/is', '', $start );
     573                if ( preg_match( '/^<(rss|rdf:RDF)[\s>]/i', $start ) ) {
     574                        return 'application/rss+xml';
     575                }
     576                if ( preg_match( '/^<feed[\s>]/i', $start ) ) {
     577                        return 'application/atom+xml';
     578                }
     579                $generic = array( '', 'text/plain', 'application/octet-stream', 'text/html' );
     580                if ( in_array( $content_type, $generic, true ) && '{' === substr( $start, 0, 1 ) ) {
     581                        $json = json_decode( $body, true );
     582                        if ( is_array( $json ) && isset( $json['version'] ) && is_string( $json['version'] ) && 0 === strpos( $json['version'], 'https://jsonfeed.org/version/' ) ) {
     583                                return 'application/feed+json';
     584                        }
     585                        if ( is_array( $json ) && isset( $json['items'][0]['type'], $json['items'][0]['properties'] ) ) {
     586                                return 'application/mf2+json';
     587                        }
     588                }
     589                return $content_type;
     590        }
     591}
     592
     593if ( ! function_exists( __NAMESPACE__ . '\\name_is_content_prefix' ) ) {
     594        /**
     595         * Checks whether a name only repeats the start of the content.
     596         *
     597         * Ignores a trailing ellipsis (... or …) on the name, as on truncated
     598         * titles, and ignores whitespace in both.
     599         *
     600         * @since 2.0.0
     601         *
     602         * @param mixed $name    Name.
     603         * @param mixed $content Content text.
     604         * @return bool True if both are non-empty strings and the content starts
     605         *              with the name.
     606         */
     607        function name_is_content_prefix( $name, $content ) {
     608                if ( ! is_string( $name ) || ! is_string( $content ) ) {
     609                        return false;
     610                }
     611                $name    = preg_replace( '/\\s+/u', '', preg_replace( '/\\s*(\\.\\.\\.|…)\\s*$/u', '', $name ) );
     612                $content = preg_replace( '/\\s+/u', '', $content );
     613                return '' !== $name && '' !== $content && 0 === strpos( $content, $name );
     614        }
     615}
     616
     617if ( ! function_exists( __NAMESPACE__ . '\\post_type_discovery' ) ) {
     618        /**
     619         * Determines the IndieWeb post type of a jf2 or mf2 entry.
     620         *
     621         * Events, reviews and recipes are their own type. For an entry, response
     622         * properties decide the type first (review-of, rsvp, checkin, like-of,
     623         * in-reply-to, ...), then media (video, photo, audio). An entry with a name
     624         * that is not a prefix of its content (or summary) is an article; anything
     625         * else is a note.
     626         *
     627         * @since 1.0.0
     628         * @since 2.0.0 Adds review (from review-of, or the review type) and recipe,
     629         *              and follows the spec for a name without content.
     630         *
     631         * @link https://indieweb.org/post-type-discovery
     632         *
     633         * @param array|mixed $jf2 jf2 object, or an mf2 object (converted first).
     634         * @return string The post type, or an empty string for other types.
     635         */
    337636        function post_type_discovery( $jf2 ) {
    338637                if ( ! is_array( $jf2 ) ) {
    … …  
    345644                        return '';
    346645                }
    347                 if ( 'event' === $jf2['type'] ) {
    348                         return 'event';
     646                // Events (as in the spec), and reviews and recipes (as XRay does), are their own type.
     647                if ( in_array( $jf2['type'], array( 'event', 'review', 'recipe' ), true ) ) {
     648                        return $jf2['type'];
    349649                }
    350650                if ( 'entry' === $jf2['type'] ) {
    351651                        $map = array(
     652                                'review'    => array( 'review-of' ),
    352653                                'rsvp'      => array( 'rsvp' ),
    353654                                'checkin'   => array( 'checkin' ),
    … …  
    377678                                }
    378679                        }
    379                         if ( isset( $jf2['name'] ) && ! empty( $jf2['name'] ) ) {
    380                                 $jf2['name'] = $jf2['name'];
    381                                 $content     = ifset( $jf2['content'] );
    382                                 if ( ! $content ) {
    383                                         $content = ifset( $jf2['summary'] );
    384                                 }
    385                                 if ( is_array( $content ) && array_key_exists( 'text', $content ) ) {
    386                                         $content = $content['text'];
    387                                 }
    388                                 if ( is_string( $content ) ) {
    389                                         $content = trim( $content );
    390                                         if ( 0 !== strpos( $content, $jf2['name'] ) ) {
    391                                                 return 'article';
    392                                         }
    393                                 }
    394                         }
    395                                 return 'note';
     680                        // https://www.w3.org/TR/post-type-discovery/#algorithm: a name that is not
     681                        // a prefix of the content (or summary, or nothing) makes an article. A
     682                        // truncated name (ending in an ellipsis) is compared without it.
     683                        $name = ( isset( $jf2['name'] ) && is_string( $jf2['name'] ) ) ? trim( preg_replace( '/\s+/u', ' ', $jf2['name'] ) ) : '';
     684                        if ( '' !== $name ) {
     685                                $content = $jf2['content'] ?? null;
     686                                if ( is_array( $content ) ) {
     687                                        $content = $content['text'] ?? ( $content['value'] ?? null );
     688                                }
     689                                if ( ! is_string( $content ) || '' === trim( $content ) ) {
     690                                        $content = $jf2['summary'] ?? '';
     691                                }
     692                                $content = is_string( $content ) ? trim( preg_replace( '/\s+/u', ' ', $content ) ) : '';
     693                                if ( ! name_is_content_prefix( $name, $content ) ) {
     694                                        return 'article';
     695                                }
     696                        }
     697                        return 'note';
    396698                }
    397699                return '';
    … …  
    399701}
    400702
    401 if ( ! function_exists( 'seconds_to_iso8601' ) ) {
     703if ( ! function_exists( __NAMESPACE__ . '\\seconds_to_iso8601' ) ) {
     704        /**
     705         * Converts a number of seconds into an ISO 8601 duration.
     706         *
     707         * For example 3725 becomes PT1H2M5S.
     708         *
     709         * @since 1.0.0
     710         *
     711         * @param int|float $second Duration in seconds.
     712         * @return string ISO 8601 duration.
     713         */
    402714        function seconds_to_iso8601( $second ) {
    403715                $h   = intval( $second / 3600 );
    … …  
    418730}
    419731
    420 if ( ! function_exists( 'pt_load_domdocument' ) ) {
     732if ( ! function_exists( __NAMESPACE__ . '\\pt_load_domdocument' ) ) {
     733        /**
     734         * Parses HTML into a DOMDocument.
     735         *
     736         * On PHP 8.4 and later, uses PHP's own HTML5 parser (Dom\HTMLDocument),
     737         * which builds the same tree a browser does and is several times faster
     738         * than masterminds/html5 (see pt_native_html_document()). It can be turned
     739         * off with the parse_this_native_html_parser filter. Otherwise, or if that
     740         * fails, uses the bundled masterminds/html5 parser, and failing that PHP's
     741         * DOMDocument with errors suppressed.
     742         *
     743         * @since 1.0.0
     744         * @since 2.0.0 Uses PHP's HTML5 parser when available.
     745         *
     746         * @param string $content HTML.
     747         * @return DOMDocument The parsed document.
     748         */
    421749        function pt_load_domdocument( $content ) {
     750                $content = (string) $content;
     751                /**
     752                 * Filters whether to parse HTML with PHP's own HTML5 parser (PHP 8.4+).
     753                 *
     754                 * Return false to use masterminds/html5 even when PHP's parser is
     755                 * available. The two can differ on invalid markup, where PHP's parser
     756                 * follows the HTML5 algorithm (as browsers do).
     757                 *
     758                 * @since 2.0.0
     759                 *
     760                 * @param bool $enabled Whether to use it. Default true.
     761                 */
     762                if ( class_exists( 'Dom\HTMLDocument' ) && apply_filters( 'parse_this_native_html_parser', true ) ) {
     763                        $doc = pt_native_html_document( $content );
     764                        if ( $doc ) {
     765                                return $doc;
     766                        }
     767                }
    422768                if ( ! class_exists( '\Masterminds\HTML5', false ) ) {
    423769                        $file = plugin_dir_path( __DIR__ ) . 'lib/html5/autoloader.php';
    … …  
    430776                        $doc = $doc->loadHTML( $content );
    431777                } else {
    432                         $doc = new DOMDocument();
     778                        $doc = new \DOMDocument();
     779                        // DOMDocument::loadHTML() throws on an empty string in PHP 8.
     780                        if ( '' === trim( $content ) ) {
     781                                return $doc;
     782                        }
    433783                        libxml_use_internal_errors( true );
    434                         if ( function_exists( 'mb_convert_encoding' ) ) {
    435                                 $content = mb_convert_encoding( $content, 'HTML-ENTITIES', mb_detect_encoding( $content ) );
     784                        if ( function_exists( 'mb_encode_numericentity' ) ) {
     785                                // DOMDocument assumes ISO-8859-1, so convert to UTF-8 and encode non-ASCII characters as entities.
     786                                $encoding = mb_detect_encoding( $content, array( 'UTF-8', 'ISO-8859-1' ), true );
     787                                if ( $encoding && 'UTF-8' !== $encoding ) {
     788                                        $content = mb_convert_encoding( $content, 'UTF-8', $encoding );
     789                                }
     790                                $content = mb_encode_numericentity( $content, array( 0x80, 0x10FFFF, 0, 0x1FFFFF ), 'UTF-8' );
    436791                        }
    437792                        $doc->loadHTML( $content );
    … …  
    441796        }
    442797}
    443 if ( ! function_exists( 'pt_secure_rewrite' ) ) {
     798
     799if ( ! function_exists( __NAMESPACE__ . '\\pt_native_html_document' ) ) {
     800        /**
     801         * Parses HTML with PHP's HTML5 parser and returns it as a DOMDocument.
     802         *
     803         * Dom\HTMLDocument (PHP 8.4+) belongs to PHP's new DOM API, while php-mf2
     804         * and the rest of Parse This work with DOMDocument. The parsed tree is
     805         * passed across as XML, which reproduces it exactly without parsing the
     806         * HTML twice. Comments (which may contain "--") and attributes whose names
     807         * aren't valid XML (@click, x-on:click, :class) are removed first, since
     808         * XML can't hold them and nothing here reads them. Elements are kept out of
     809         * the XHTML namespace, so XPath queries such as //meta work as before.
     810         *
     811         * @since 2.0.0
     812         *
     813         * @param string $content HTML.
     814         * @return DOMDocument|null The parsed document, or null if PHP's parser
     815         *                          isn't available or the document can't be
     816         *                          passed across.
     817         */
     818        function pt_native_html_document( $content ) {
     819                if ( ! class_exists( 'Dom\HTMLDocument' ) ) {
     820                        return null;
     821                }
     822                try {
     823                        $html  = \Dom\HTMLDocument::createFromString( (string) $content, LIBXML_NOERROR | \Dom\HTML_NO_DEFAULT_NS );
     824                        $xpath = new \Dom\XPath( $html );
     825                        foreach ( iterator_to_array( $xpath->query( '//comment()' ) ) as $comment ) {
     826                                $comment->remove();
     827                        }
     828                        foreach ( iterator_to_array( $xpath->query( '//@*' ) ) as $attribute ) {
     829                                if ( ! preg_match( '/^[A-Za-z_][A-Za-z0-9_.\-]*$/', $attribute->name ) ) {
     830                                        $attribute->ownerElement->removeAttribute( $attribute->name ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHP DOM property.
     831                                }
     832                        }
     833                        $xml = $html->saveXml();
     834                } catch ( \Throwable $e ) {
     835                        return null;
     836                }
     837                $doc    = new \DOMDocument();
     838                $errors = libxml_use_internal_errors( true );
     839                $loaded = $doc->loadXML( $xml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_PARSEHUGE );
     840                libxml_clear_errors();
     841                libxml_use_internal_errors( $errors );
     842                return $loaded ? $doc : null;
     843        }
     844}
     845if ( ! function_exists( __NAMESPACE__ . '\\pt_remote_get' ) ) {
     846        /**
     847         * Retrieves a remote URL, retrying once with a browser user agent if the site rejects the request.
     848         *
     849         * Fetched documents decide which further URLs are requested (author pages,
     850         * alternates, REST API links), so this must stay on wp_safe_remote_get(),
     851         * which refuses local and private addresses. Don't switch it to
     852         * wp_remote_get().
     853         *
     854         * @param string $url         URL to retrieve.
     855         * @param array  $args        Optional. Arguments passed to wp_safe_remote_get().
     856         * @param array  $retry_codes Optional. Response codes that trigger the retry.
     857         * @return array|WP_Error The response, or WP_Error on failure.
     858         */
     859        function pt_remote_get( $url, $args = array(), $retry_codes = array( 403, 415 ) ) {
     860                $args = wp_parse_args(
     861                        $args,
     862                        array(
     863                                'timeout'             => 15,
     864                                'limit_response_size' => 1048576,
     865                                'redirection'         => 5,
     866                        )
     867                );
     868
     869                $response = wp_safe_remote_get( $url, $args );
     870                if ( is_wp_error( $response ) ) {
     871                        return $response;
     872                }
     873                if ( ! in_array( (int) wp_remote_retrieve_response_code( $response ), $retry_codes, true ) ) {
     874                        return $response;
     875                }
     876
     877                $args['user-agent'] = 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:57.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 Parse This/WP';
     878                $response           = wp_safe_remote_get( $url, $args );
     879                if ( is_wp_error( $response ) ) {
     880                        return $response;
     881                }
     882                $code = (int) wp_remote_retrieve_response_code( $response );
     883                if ( in_array( $code, $retry_codes, true ) ) {
     884                        return new \WP_Error( 'source_error', __( 'Unable to retrieve the URL.', 'parse-this' ), array( 'response_code' => $code ) );
     885                }
     886                return $response;
     887        }
     888}
     889
     890if ( ! function_exists( __NAMESPACE__ . '\\pt_secure_rewrite' ) ) {
     891        /**
     892         * Upgrades http:// URLs to https:// for hosts known to support HTTPS.
     893         *
     894         * @since 1.0.1
     895         *
     896         * @param string $url URL.
     897         * @return string The URL, with https:// if its domain is on the list.
     898         */
    444899        function pt_secure_rewrite( $url ) {
    445900                $host   = wp_parse_url( $url, PHP_URL_HOST );
    … …  
    460915                        'google.com',
    461916                        'gravatar.com',
    462                         'gstatic.com',
     917                        'gstatic.com', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- A domain to rewrite to https, not content loaded from it.
    463918                        'kernel.org',
    464919                        'lwn.net',
    465920                        'tumblr.com',
    466921                        'twitter.com',
     922                        'x.com',
    467923                        'vimeo.com',
    468924                        'wikipedia.org',
    … …  
    470926                        'youtube.com',
    471927                );
     928                /**
     929                 * Filters the domains whose http:// URLs are upgraded to https://.
     930                 *
     931                 * Matching is on the registrable domain, so listing example.com also
     932                 * covers www.example.com.
     933                 *
     934                 * @since 1.0.1
     935                 *
     936                 * @param string[] $secure Domains known to support HTTPS.
     937                 */
    472938                $secure = apply_filters( 'pt_rewrite_secure', $secure );
    473939                if ( in_array( $host, $secure, true ) ) {
    … …  
    479945
    480946
    481 /**
    482  * Parses Link Headers
    483  *
    484  *
    485  * @return array
    486  */
    487 
    488 if ( ! function_exists( 'pt_parse_header_links' ) ) {
     947if ( ! function_exists( __NAMESPACE__ . '\\pt_parse_header_links' ) ) {
     948        /**
     949         * Parses HTTP Link headers.
     950         *
     951         * @since 2.0.0
     952         *
     953         * @link https://www.rfc-editor.org/rfc/rfc8288
     954         *
     955         * @param string|string[] $links One Link header value, or several. A value may
     956         *                               hold multiple comma-separated links.
     957         * @return array[] One entry per link and rel value, with 'uri', 'rel' and any
     958         *                 other parameters (type, title, ...).
     959         */
    489960        function pt_parse_header_links( $links ) {
    490961                $items = array();
    491962
     963                if ( is_string( $links ) ) {
     964                        $links = array( $links );
     965                }
     966
    492967                if ( is_array( $links ) && 1 <= count( $links ) ) {
     968                        // A header may hold several comma-separated links. Only split on commas that start a new <uri>, as URIs may contain commas.
     969                        $split = array();
    493970                        foreach ( $links as $link ) {
    494                                 $item   = array();
    495                                 $pieces = explode( ';', $link );
    496                                 $uri    = array_shift( $pieces );
     971                                $split = array_merge( $split, preg_split( '/,(?=\s*<)/', $link ) );
     972                        }
     973                        foreach ( $split as $link ) {
     974                                $item = array();
     975                                if ( preg_match( '/^\s*<([^>]*)>(.*)$/s', $link, $match ) ) {
     976                                        $uri    = $match[1];
     977                                        $pieces = explode( ';', $match[2] );
     978                                } else {
     979                                        $pieces = explode( ';', $link );
     980                                        $uri    = array_shift( $pieces );
     981                                }
    497982                                foreach ( $pieces as $p ) {
    498                                         $elements = explode( '=', $p );
    499 
    500                                         $item[ trim( $elements[0] ) ] = trim( $elements[1], '"\'' );
     983                                        $elements = explode( '=', $p, 2 );
     984                                        $name     = trim( $elements[0] );
     985                                        if ( '' === $name ) {
     986                                                continue;
     987                                        }
     988
     989                                        $item[ $name ] = isset( $elements[1] ) ? trim( $elements[1], " \t\"'" ) : '';
    501990                                }
    502991
    … …  
    5191008}
    5201009
    521 /**
    522  * Find WordPress REST Alternate
    523  *
    524  *
    525  * @return string|false
    526  */
    527 
    528 if ( ! function_exists( 'pt_find_rest_alternate' ) ) {
     1010if ( ! function_exists( __NAMESPACE__ . '\\pt_find_rest_alternate' ) ) {
     1011        /**
     1012         * Finds the application/json alternate link among parsed Link headers.
     1013         *
     1014         * WordPress sends this for each post, pointing at its REST API URL.
     1015         *
     1016         * @since 2.0.0
     1017         *
     1018         * @param array[] $links Links from pt_parse_header_links().
     1019         * @return string|false The URL, or false if there is none.
     1020         */
    5291021        function pt_find_rest_alternate( $links ) {
    5301022                foreach ( $links as $link ) {
    531                         if ( 'alternate' === ifset( $link['rel'] ) && 'application/json' === ifset( $link['type'] ) ) {
     1023                        if ( 'alternate' === ( $link['rel'] ?? null ) && 'application/json' === ( $link['type'] ?? null ) ) {
    5321024                                return $link['uri'];
    5331025                        }
    … …  
    5381030}
    5391031
    540 /**
    541  * Find WordPress REST Endpoint
    542  *
    543  *
    544  * @return string|false
    545  */
    546 
    547 if ( ! function_exists( 'pt_find_rest_endpoint' ) ) {
     1032if ( ! function_exists( __NAMESPACE__ . '\\pt_find_rest_endpoint' ) ) {
     1033        /**
     1034         * Finds the WordPress REST API root among parsed Link headers.
     1035         *
     1036         * @since 2.0.0
     1037         *
     1038         * @param array[] $links Links from pt_parse_header_links().
     1039         * @return string|false The REST API root URL, or false if there is none.
     1040         */
    5481041        function pt_find_rest_endpoint( $links ) {
    5491042                foreach ( $links as $link ) {
    550                         if ( 'https://api.w.org/' === ifset( $link['rel'] ) ) {
     1043                        if ( 'https://api.w.org/' === ( $link['rel'] ?? null ) ) {
    5511044                                return $link['uri'];
    5521045                        }
    … …  
    5561049}
    5571050
    558 if ( ! function_exists( 'pt_make_absolute_url' ) ) {
     1051if ( ! function_exists( __NAMESPACE__ . '\\pt_absolute_urls_in_html' ) ) {
     1052        /**
     1053         * Makes the links and media URLs in an HTML fragment absolute.
     1054         *
     1055         * Resolves href, src and poster attributes against a base URL, for content
     1056         * taken from a document (such as a JSON Feed item) whose own URL differs
     1057         * from where it will be shown.
     1058         *
     1059         * @since 2.0.0
     1060         *
     1061         * @param string $html HTML fragment.
     1062         * @param string $base Base URL.
     1063         * @return string The HTML with absolute URLs, or $html unchanged if it has
     1064         *                none to resolve or $base is not a URL.
     1065         */
     1066        function pt_absolute_urls_in_html( $html, $base ) {
     1067                if ( ! is_string( $html ) || '' === trim( $html ) || ! wp_http_validate_url( $base ) || ! preg_match( '/\s(href|src|poster)\s*=/i', $html ) ) {
     1068                        return $html;
     1069                }
     1070                $doc  = pt_load_domdocument( '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>' . $html . '</body></html>' );
     1071                $body = $doc->getElementsByTagName( 'body' )->item( 0 );
     1072                if ( ! $body ) {
     1073                        return $html;
     1074                }
     1075                foreach ( ( new \DOMXPath( $doc ) )->query( '//body//*[@href or @src or @poster]' ) as $element ) {
     1076                        foreach ( array( 'href', 'src', 'poster' ) as $attribute ) {
     1077                                $value = $element->getAttribute( $attribute );
     1078                                if ( '' !== $value && '#' !== $value[0] ) {
     1079                                        $element->setAttribute( $attribute, pt_make_absolute_url( $value, $base ) );
     1080                                }
     1081                        }
     1082                }
     1083                $out = '';
     1084                foreach ( $body->childNodes as $node ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHP DOM property.
     1085                        $out .= $doc->saveHTML( $node );
     1086                }
     1087                return $out;
     1088        }
     1089}
     1090
     1091if ( ! function_exists( __NAMESPACE__ . '\\pt_make_absolute_url' ) ) {
     1092        /**
     1093         * Resolves a relative URL against a base URL.
     1094         *
     1095         * @since 2.0.0
     1096         *
     1097         * @param string $maybe_relative_path URL or path to resolve.
     1098         * @param string $url                 Base URL.
     1099         * @return string The absolute URL, or $maybe_relative_path unchanged if it is
     1100         *                already absolute or either value cannot be parsed.
     1101         */
    5591102        function pt_make_absolute_url( $maybe_relative_path, $url ) {
    5601103                if ( empty( $url ) ) {
    … …  
    5921135                        }
    5931136                }
    594                
     1137
    5951138                // Start off with the absolute URL path.
    5961139                $path = ! empty( $url_parts['path'] ) ? $url_parts['path'] : '/';
    … …  
    6001143                        $path = $relative_url_parts['path'];
    6011144
    602                 // Else it's a relative path.
     1145                        // Else it's a relative path.
    6031146                } elseif ( ! empty( $relative_url_parts['path'] ) ) {
    6041147                        // Strip off any file components from the absolute path.
    … …  
    6271170                }
    6281171
    629 
    6301172                return $absolute_path . '/' . ltrim( $path, '/' );
    6311173        }
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/html5/HTML5.php

    r2351228 r3738687  
    147147     *
    148148     * @param string $input
    149      * @param array  $options
    150149     *
    151150     * @return \DOMDocument
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/html5/HTML5/Elements.php

    r2110745 r3738687  
    7171     */
    7272    const BLOCK_ONLY_INLINE = 128;
     73
     74    /**
     75     * Elements with optional end tags that cause auto-closing of previous and parent tags,
     76     * as example most of the table related tags, see https://www.w3.org/TR/html401/struct/tables.html
     77     * Structure is as follows:
     78     * TAG-NAME => [PARENT-TAG-NAME-TO-CLOSE1, PARENT-TAG-NAME-TO-CLOSE2, ...].
     79     *
     80     * Order is important, after auto-closing one parent with might have to close also their parent.
     81     *
     82     * @var array<string, string[]>
     83     */
     84    public static $optionalEndElementsParentsToClose = array(
     85        'tr' => array('td', 'tr'),
     86        'td' => array('td', 'th'),
     87        'th' => array('td', 'th'),
     88        'tfoot' => array('td', 'th', 'tr', 'tbody', 'thead'),
     89        'tbody' => array('td', 'th', 'tr', 'thead'),
     90    );
    7391
    7492    /**
    … …  
    186204        'ul' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
    187205        'var' => 1,
    188         'video' => 65, // NORMAL | BLOCK_TAG
     206        'video' => 1,
    189207        'wbr' => 9, // NORMAL | VOID_TAG
    190208
    … …  
    293311        'feDisplacementMap' => 1,
    294312        'feDistantLight' => 1,
     313        'feDropShadow' => 1,
    295314        'feFlood' => 1,
    296315        'feFuncA' => 1,
    … …  
    446465        'fedisplacementmap' => 'feDisplacementMap',
    447466        'fedistantlight' => 'feDistantLight',
     467        'fedropshadow' => 'feDropShadow',
    448468        'feflood' => 'feFlood',
    449469        'fefunca' => 'feFuncA',
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/html5/HTML5/Parser/CharacterReference.php

    r2110745 r3738687  
    4949
    5050    /**
    51      * Given a hexidecimal number, return the UTF-8 character.
     51     * Given a hexadecimal number, return the UTF-8 character.
    5252     *
    5353     * @param $hexdec
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/html5/HTML5/Parser/DOMTreeBuilder.php

    r2623705 r3738687  
    232232     * This is used for handling Processor Instructions as they are
    233233     * inserted. If omitted, PI's are inserted directly into the DOM tree.
    234      *
    235      * @param InstructionProcessor $proc
    236234     */
    237235    public function setInstructionProcessor(InstructionProcessor $proc)
    … …  
    360358        }
    361359
     360        // some elements as table related tags might have optional end tags that force us to auto close multiple tags
     361        // https://www.w3.org/TR/html401/struct/tables.html
     362        if ($this->current instanceof \DOMElement && isset(Elements::$optionalEndElementsParentsToClose[$lname])) {
     363            foreach (Elements::$optionalEndElementsParentsToClose[$lname] as $parentElName) {
     364                if ($this->current instanceof \DOMElement && $this->current->tagName === $parentElName) {
     365                    $this->autoclose($parentElName);
     366                }
     367            }
     368        }
     369
    362370        try {
    363371            $prefix = ($pos = strpos($lname, ':')) ? substr($lname, 0, $pos) : '';
    … …  
    390398        if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) {
    391399            // PHP tends to free the memory used by DOM,
    392             // to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes
     400            // to avoid spl_object_id collisions we have to avoid garbage collection of $ele storing it into $pushes
    393401            // see https://bugs.php.net/bug.php?id=67459
    394             $this->pushes[spl_object_hash($ele)] = array($pushes, $ele);
     402            $this->pushes[spl_object_id($ele)] = array($pushes, $ele);
    395403        }
    396404
    … …  
    514522        }
    515523
    516         $cid = spl_object_hash($this->current);
     524        $cid = spl_object_id($this->current);
    517525
    518526        // XXX: HTML has no parent. What do we do, though,
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/html5/HTML5/Parser/StringInputStream.php

    r2110745 r3738687  
    184184     * @return string The current character.
    185185     */
     186    #[\ReturnTypeWillChange]
    186187    public function current()
    187188    {
    … …  
    193194     * This is part of the Iterator interface.
    194195     */
     196    #[\ReturnTypeWillChange]
    195197    public function next()
    196198    {
    … …  
    201203     * Rewind to the start of the string.
    202204     */
     205    #[\ReturnTypeWillChange]
    203206    public function rewind()
    204207    {
    … …  
    211214     * @return bool Whether the current pointer location is valid.
    212215     */
     216    #[\ReturnTypeWillChange]
    213217    public function valid()
    214218    {
    … …  
    325329    }
    326330
     331    #[\ReturnTypeWillChange]
    327332    public function key()
    328333    {
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/html5/HTML5/Parser/Tokenizer.php

    r2839509 r3738687  
    132132            $tok = $this->scanner->next();
    133133
    134             if ('!' === $tok) {
     134            if (false === $tok) {
     135                // end of string
     136                $this->parseError('Illegal tag opening');
     137            } elseif ('!' === $tok) {
    135138                $this->markupDeclaration();
    136139            } elseif ('/' === $tok) {
    … …  
    138141            } elseif ('?' === $tok) {
    139142                $this->processingInstruction();
    140             } elseif (ctype_alpha($tok)) {
     143            } elseif ($this->is_alpha($tok)) {
    141144                $this->tagName();
    142145            } else {
    … …  
    348351        // EOF -> parse error
    349352        // -> parse error
    350         if (!ctype_alpha($tok)) {
     353        if (!$this->is_alpha($tok)) {
    351354            $this->parseError("Expected tag name, got '%s'", $tok);
    352355            if ("\0" == $tok || false === $tok) {
    … …  
    505508
    506509        $val = $this->attributeValue();
    507         if ($isValidAttribute) {
     510        if ($isValidAttribute && !array_key_exists($name, $attributes)) {
    508511            $attributes[$name] = $val;
    509512        }
    … …  
    727730        if ('!' == $this->scanner->current() && '>' == $this->scanner->peek()) {
    728731            $this->scanner->consume(); // Consume the last '>'
     732
    729733            return true;
    730734        }
    … …  
    11251129            }
    11261130
    1127             // Hexidecimal encoding.
     1131            // Hexadecimal encoding.
    11281132            // X[0-9a-fA-F]+;
    11291133            // x[0-9a-fA-F]+;
    … …  
    11951199        return '&';
    11961200    }
     1201
     1202    /**
     1203     * Checks whether a (single-byte) character is an ASCII letter or not.
     1204     *
     1205     * @param string $input A single-byte string
     1206     *
     1207     * @return bool True if it is a letter, False otherwise
     1208     */
     1209    protected function is_alpha($input)
     1210    {
     1211        if (!is_string($input) || 1 !== strlen($input)) {
     1212            return false;
     1213        }
     1214
     1215        $code = ord($input);
     1216
     1217        return ($code >= 97 && $code <= 122) || ($code >= 65 && $code <= 90);
     1218    }
    11971219}
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/html5/HTML5/Parser/TreeBuildingRules.php

    r2110745 r3738687  
    8181            case 'tfoot':
    8282            case 'table': // Spec isn't explicit about this, but it's necessary.
    83 
    8483                return $this->closeIfCurrentMatches($new, $current, array(
    8584                    'thead',
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/html5/HTML5/Parser/UTF8Utils.php

    r2110745 r3738687  
    3939    /**
    4040     * Count the number of characters in a string.
    41      * UTF-8 aware. This will try (in order) iconv, MB, libxml, and finally a custom counter.
     41     * UTF-8 aware. This will try (in order) iconv, MB, and finally a custom counter.
    4242     *
    4343     * @param string $string
    … …  
    5454        if (function_exists('iconv_strlen')) {
    5555            return iconv_strlen($string, 'utf-8');
    56         }
    57 
    58         if (function_exists('utf8_decode')) {
    59             // MPB: Will this work? Won't certain decodes lead to two chars
    60             // extrapolated out of 2-byte chars?
    61             return strlen(utf8_decode($string));
    6256        }
    6357
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/html5/HTML5/Serializer/OutputRules.php

    r2351228 r3738687  
    1717class OutputRules implements RulesInterface
    1818{
    19     /**
    20      * Defined in http://www.w3.org/TR/html51/infrastructure.html#html-namespace-0.
    21      */
    2219    const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml';
    2320
    … …  
    5047
    5148    const IM_IN_MATHML = 3;
    52 
    53     /**
    54      * Used as cache to detect if is available ENT_HTML5.
    55      *
    56      * @var bool
    57      */
    58     private $hasHTML5 = false;
    5949
    6050    protected $traverser;
    … …  
    168158        $this->outputMode = static::IM_IN_HTML;
    169159        $this->out = $output;
    170         $this->hasHTML5 = defined('ENT_HTML5');
    171160    }
    172161
    … …  
    207196    }
    208197
     198    /**
     199     * @param \DOMElement $ele
     200     */
    209201    public function element($ele)
    210202    {
    … …  
    228220
    229221        $this->openTag($ele);
     222        // The tag is already self-closed (`<svg />` or `<math />`) in `openTag` if there are no child nodes.
     223        $handledAsVoidTag = $this->outputMode !== static::IM_IN_HTML && !$ele->hasChildNodes();
     224
    230225        if (Elements::isA($name, Elements::TEXT_RAW)) {
    231226            foreach ($ele->childNodes as $child) {
    … …  
    249244
    250245        // If not unary, add a closing tag.
    251         if (!Elements::isA($name, Elements::VOID_TAG)) {
     246        if (!$handledAsVoidTag && !Elements::isA($name, Elements::VOID_TAG)) {
    252247            $this->closeTag($ele);
    253248        }
    … …  
    449444    protected function wr($text)
    450445    {
    451         fwrite($this->out, $text);
     446        fwrite($this->out, (string) $text);
    452447
    453448        return $this;
    … …  
    484479     *      encoding here will just escape &'<>".
    485480     *
    486      *      Note, PHP 5.4+ has better html5 encoding.
    487      *
    488      * @todo Use the Entities class in php 5.3 to have html5 entities.
    489      *
    490481     * @param string $text      Text to encode.
    491      * @param bool   $attribute True if we are encoding an attrubute, false otherwise.
     482     * @param bool   $attribute True if we are encoding an attribute, false otherwise.
    492483     *
    493484     * @return string The encoded text.
    … …  
    500491        }
    501492
    502         // If we are in PHP 5.4+ we can use the native html5 entity functionality to
    503         // convert the named character references.
    504 
    505         if ($this->hasHTML5) {
    506             return htmlentities($text, ENT_HTML5 | ENT_SUBSTITUTE | ENT_QUOTES, 'UTF-8', false);
    507         }         // If a version earlier than 5.4 html5 entities are not entirely handled.
    508         // This manually handles them.
    509         else {
    510             return strtr($text, HTML5Entities::$map);
    511         }
     493        return htmlentities($text, ENT_HTML5 | ENT_SUBSTITUTE | ENT_QUOTES, 'UTF-8', false);
    512494    }
    513495
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/lib/mf2/Parser.php

    r2839509 r3738687  
    146146
    147147/**
    148  * Registered with the XPath object and used within XPaths for finding root elements.
    149  * @param string $class
    150  * @return bool
    151  */
    152 function classHasMf2RootClassname($class) {
    153         return count(mfNamesFromClass($class, 'h-')) > 0;
    154 }
    155 
    156 /**
    157148 * Get Nested µf Property Name From Class
    158149 *
    … …  
    365356         */
    366357        public function __construct($input, $url = null, $jsonMode = false) {
    367                 $emptyDocDefault = '<html><body></body></html>';
    368358                libxml_use_internal_errors(true);
    369359                if (is_string($input)) {
    370                         if (empty($input)) {
    371                                         $input = $emptyDocDefault;
    372                         }
    373                                
    374360                        if (class_exists('Masterminds\\HTML5')) {
    375361                                        $doc = new \Masterminds\HTML5(array('disable_html_ns' => true));
    … …  
    383369                } else {
    384370                        $doc = new DOMDocument();
    385                         @$doc->loadHTML($emptyDocDefault);
    386                 }
    387 
    388                 // Create an XPath object and allow some PHP functions to be used within XPath queries.
     371                        @$doc->loadHTML('');
     372                }
     373
    389374                $this->xpath = new DOMXPath($doc);
    390                 $this->xpath->registerNamespace('php', 'http://php.net/xpath');
    391                 $this->xpath->registerPhpFunctions('\\Mf2\\classHasMf2RootClassname');
    392375
    393376                $baseurl = $url;
    … …  
    11821165                        'properties' => $return
    11831166                );
    1184 
     1167               
    11851168                if(trim($e->getAttribute('id')) !== '') {
    11861169                        $parsed['id'] = trim($e->getAttribute("id"));
    … …  
    15241507                // start with mf2 root class name xpath
    15251508                $xpaths = array(
    1526                         '(php:function("\\Mf2\\classHasMf2RootClassname", normalize-space(@class)))'
     1509                        'contains(concat(" ",normalize-space(@class)), " h-")'
    15271510                );
    15281511
    … …  
    15791562                                break;
    15801563
    1581                                 case 'hfeed':
    1582                                         $this->upgradeRelTagToCategory($el);
    1583                                 break;
    1584 
    1585                                 case 'hproduct':
    1586                                         $review_and_hreview_aggregate = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " review ") and contains(concat(" ", normalize-space(@class), " "), " hreview-aggregate ")]', $el);
    1587 
    1588                                         if ( $review_and_hreview_aggregate->length ) {
    1589                                                 foreach ( $review_and_hreview_aggregate as $tempEl ) {
    1590                                                         if ( !$this->hasRootMf2($tempEl) ) {
    1591                                                                 $this->backcompat($tempEl, 'hreview-aggregate');
    1592                                                                 $this->addMfClasses($tempEl, 'p-review h-review-aggregate');
    1593                                                                 $this->addUpgraded($tempEl, array('review hreview-aggregate'));
    1594                                                         }
    1595                                                 }
    1596                                         }
    1597 
    1598                                         $review_and_hreview = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " review ") and contains(concat(" ", normalize-space(@class), " "), " hreview ")]', $el);
    1599 
    1600                                         if ( $review_and_hreview->length ) {
    1601                                                 foreach ( $review_and_hreview as $tempEl ) {
    1602                                                         if ( !$this->hasRootMf2($tempEl) ) {
    1603                                                                 $this->backcompat($tempEl, 'hreview');
    1604                                                                 $this->addMfClasses($tempEl, 'p-review h-review');
    1605                                                                 $this->addUpgraded($tempEl, array('review hreview'));
    1606                                                         }
    1607                                                 }
    1608                                         }
    1609 
    1610                                 break;
    1611 
    1612                                 case 'hreview-aggregate':
    16131564                                case 'hreview':
    16141565                                        $item_and_vcard = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " item ") and contains(concat(" ", normalize-space(@class), " "), " vcard ")]', $el);
    … …  
    16521603
    16531604                                case 'vevent':
    1654                                         $location_and_vcard = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " location ") and contains(concat(" ", normalize-space(@class), " "), " vcard ")]', $el);
    1655 
    1656                                         if ( $location_and_vcard->length ) {
    1657                                                 foreach ( $location_and_vcard as $tempEl ) {
     1605                                        $location = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " location ")]', $el);
     1606
     1607                                        if ( $location->length ) {
     1608                                                foreach ( $location as $tempEl ) {
    16581609                                                        if ( !$this->hasRootMf2($tempEl) ) {
    1659                                                                 $this->addMfClasses($tempEl, 'p-location h-card');
     1610                                                                $this->addMfClasses($tempEl, 'h-card');
    16601611                                                                $this->backcompat($tempEl, 'vcard');
    16611612                                                        }
    … …  
    17371688        public function hasRootMf2(\DOMElement $el) {
    17381689                $class = str_replace(array("\t", "\n"), ' ', $el->getAttribute('class'));
    1739 
    1740                 // Check for valid mf2 root classnames, not just any classname with a h- prefix.
    1741                 return count(mfNamesFromClass($class, 'h-')) > 0;
     1690                $classes = array_filter(explode(' ', $class));
     1691
     1692                foreach ( $classes as $classname ) {
     1693                        if ( strpos($classname, 'h-') === 0 ) {
     1694                                return true;
     1695                        }
     1696                }
     1697
     1698                return false;
    17421699        }
    17431700
    … …  
    17991756                'vevent' => 'h-event',
    18001757                'hreview' => 'h-review',
    1801                 'hreview-aggregate' => 'h-review-aggregate',
    18021758                'hproduct' => 'h-product',
    18031759                'adr' => 'h-adr',
    1804                 'geo' => 'h-geo'
    18051760        );
    18061761
    … …  
    19191874                ),
    19201875                'hfeed' => array(
    1921                         'author' => array(
    1922                                 'replace' => 'p-author h-card',
    1923                                 'context' => 'vcard'
    1924                         ),
    1925                         'url' => array(
    1926                                 'replace' => 'u-url'
    1927                         ),
    1928                         'photo' => array(
    1929                                 'replace' => 'u-photo'
    1930                         ),
    1931                         'category' => array(
    1932                                 'replace' => 'p-category'
    1933                         ),
     1876                        # nothing currently
    19341877                ),
    19351878                'hentry' => array(
    … …  
    20411984                        ),
    20421985                        'location' => array(
    2043                                 'replace' => 'p-location',
     1986                                'replace' => 'h-card',
     1987                                'context' => 'vcard'
    20441988                        ),
    20451989                        'geo' => array(
    20461990                                'replace' => 'p-location h-geo'
    20471991                        ),
    2048                         'attendee' => array(
    2049                                 'replace' => 'p-attendee h-card',
    2050                                 'context' => 'vcard'
    2051                         )
    20521992                ),
    20531993                'hreview' => array(
    … …  
    20852025                        ),
    20862026                ),
    2087                 'hreview-aggregate' => array(
    2088                         'summary' => array(
    2089                                 'replace' => 'p-name'
    2090                         ),
    2091                         # fn: see item.fn below
    2092                         # photo: see item.photo below
    2093                         # url: see item.url below
    2094                         'item' => array(
    2095                                 'replace' => 'p-item h-item',
    2096                                 'context' => 'item'
    2097                         ),
    2098                         'rating' => array(
    2099                                 'replace' => 'p-rating'
    2100                         ),
    2101                         'best' => array(
    2102                                 'replace' => 'p-best'
    2103                         ),
    2104                         'worst' => array(
    2105                                 'replace' => 'p-worst'
    2106                         ),
    2107                         'average' => array(
    2108                                 'replace' => 'p-average'
    2109                         ),
    2110                         'count' => array(
    2111                                 'replace' => 'p-count'
    2112                         ),
    2113                         'votes' => array(
    2114                                 'replace' => 'p-votes'
    2115                         ),
    2116                 ),
    21172027                'hproduct' => array(
    21182028                        'fn' => array(
    … …  
    21372047                                'replace' => 'u-url',
    21382048                        ),
    2139                         // review is handled in the special processing section to allow for 'review hreview-aggregate'
     2049                        'review' => array(
     2050                                'replace' => 'p-review h-review',
     2051                        ),
    21402052                        'price' => array(
    21412053                                'replace' => 'p-price'
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/parse-this.php

    r2507837 r3738687  
    33 * Plugin Name: Parse This
    44 * Plugin URI: https://github.com/dshanske/parse-this
    5  * Description:
    6  * Version: 1.0.1
     5 * Description: Turns URLs into structured jf2 data from microformats2, JSON-LD, meta tags and RSS, Atom and JSON feeds.
     6 * Version: 2.0.0
    77 * Author: David Shanske
    88 * Author URI: https://david.shanske.com
     9 * Requires at least: 6.2
     10 * Requires PHP: 7.4
     11 * License: GPLv2 or later
     12 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
    913 * Text Domain: parse-this
    10  * Domain Path:  /languages
     14 *
     15 * @package Parse_This
    1116 */
    1217
    13 
    14 /*
    15  Parse This Load
    16  */
     18defined( 'ABSPATH' ) || exit;
    1719
    1820if ( ! function_exists( 'parse_this_loader' ) ) {
     21        /**
     22         * Loads Parse This.
     23         *
     24         * Runs on plugins_loaded at priority 9, so that the standalone plugin loads
     25         * ahead of copies bundled in other plugins (Post Kinds loads its copy at
     26         * priority 11). Functions are guarded by function_exists() and the first
     27         * autoloader registered serves the classes, so whichever copy loads first
     28         * wins. It also registers the deprecated global names in aliases.php, so
     29         * that plugins bundling an older copy use this one.
     30         *
     31         * Defines PARSE_THIS_VERSION for the copy that loads, and fires
     32         * parse_this_loaded once everything is available. A copy older than 2.0.0
     33         * defines neither, so a plugin can tell when an older bundled copy won.
     34         *
     35         * @since 1.0.0
     36         * @since 2.0.0 Defines PARSE_THIS_VERSION and fires parse_this_loaded.
     37         */
    1938        function parse_this_loader() {
     39                if ( ! defined( 'PARSE_THIS_VERSION' ) ) {
     40                        /**
     41                         * Version of the copy of Parse This that loaded. Keep in step with the
     42                         * Version header.
     43                         *
     44                         * @since 2.0.0
     45                         * @var string
     46                         */
     47                        define( 'PARSE_THIS_VERSION', '2.0.0' );
     48                }
    2049                require_once plugin_dir_path( __FILE__ ) . 'includes/autoload.php';
     50                require_once plugin_dir_path( __FILE__ ) . 'includes/functions.php';
    2151
    22                 // Functions Not Available in Earlier Versions of WordPress
    23                 require_once plugin_dir_path( __FILE__ ) . 'includes/compat-functions.php';
     52                new ParseThis\REST_Endpoint();
    2453
    25                 require_once plugin_dir_path( __FILE__ ) . 'includes/functions.php';
    26                 // Parse This REST Endpoint
    27                 require_once plugin_dir_path( __FILE__ ) . 'includes/class-rest-parse-this.php';
    28 
     54                /**
     55                 * Fires once Parse This has loaded: its classes, functions and REST route
     56                 * are available.
     57                 *
     58                 * Plugins that use Parse This can hook here, or check PARSE_THIS_VERSION,
     59                 * rather than depend on load order.
     60                 *
     61                 * @since 2.0.0
     62                 *
     63                 * @param string $version Version of the copy of Parse This that loaded.
     64                 */
     65                do_action( 'parse_this_loaded', PARSE_THIS_VERSION );
    2966        }
    3067        add_action( 'plugins_loaded', 'parse_this_loader', 9 );
    3168}
    32 
  • indieweb-post-kinds/tags/4.0.0/lib/parse-this/readme.txt

    r2507837 r3738687  
    11=== Parse This ===
    22Contributors: dshanske
    3 Tags: indieweb
    4 Stable tag: trunk
    5 Requires at least: 4.9
    6 Requires PHP: 5.6
    7 Tested up to: 5.6
     3Tags: indieweb, microformats, jf2, json-ld, feeds
     4Stable tag: 2.0.0
     5Requires at least: 6.2
     6Requires PHP: 7.4
     7Tested up to: 7.1
    88License: GPLv2 or later
    9 License URI: http://www.gnu.org/licenses/gpl-2.0.html
    10 
    11 Parse This turns URLs into structured jf2 data
    12 
    13 == Description ==
    14 
    15 Parse This is based on a variety of projects including the parsing code from Press This, which was removed from WordPress.
    16 
    17 * It supports parsing from MF2 if present
    18 * For sites that are not marked up with Microformats 2(MF2) it will fall back onto parsing JSON-LD, then HTML/OpenGraph/Dublin Core Tags/etc.
    19 * It supports parsing of JSONFeed and RSS/Atom feeds
    20 * It supports parsing of  WordPress REST API endpoints to generate a site feed
    21 
    22 The goal is to produce structured jf2 data that can be used for previewing links as well as feed readers and other options. It is also bundled in the Post Kinds and Yarns Microsub plugins as a library.
    23 
    24 It can be installed as a standalone plugin which will provide the necessary libraries and functionality as well as the REST API endpoint for getting JF2 data from an arbitrary URL or a WordPress Post.
    25 
     9License URI: https://www.gnu.org/licenses/gpl-2.0.html
     10
     11Parse This turns URLs into structured jf2 data.
     12
     13== Description ==
     14
     15Parse This fetches a URL and turns it into [jf2](https://jf2.spec.indieweb.org/), a simple JSON format for posts, people and feeds. The result can be used for link previews, feed readers, replies and likes, and similar features.
     16
     17It started from the parsing code in Press This, which was removed from WordPress, and has grown from there. It runs as a standalone plugin that other plugins, such as Post Kinds and Yarns Microsub, can require or bundle.
     18
     19It also runs on ClassicPress 2.x.
     20
     21= What it parses =
     22
     23* **Microformats2.** When a page is marked up with microformats, they are used first: entries, cards, events, reviews (including h-entry reviews with `review-of`), recipes, products and any other type. On a busy page it picks the item the page is about, and it follows the IndieWeb authorship algorithm.
     24* **Other metadata.** Other sources only fill in what microformats don't provide: a site-specific parser for YouTube or X (Twitter), then JSON-LD (articles, events, reviews, products, recipes, media and people) and Open Graph, Dublin Core and other meta tags. If there is still no content, the page's WordPress REST API version is fetched, when the site advertises one.
     25* **Parts of pages.** A link with a fragment (`#comment-12`) is parsed as just that part of the page. A WordPress comment link whose page has no microformats for it is read from the site's REST API as a reply.
     26* **Feeds.** RSS and Atom (through WordPress's SimplePie), JSON Feed 1 and 1.1, jf2 and mf2 JSON, and WordPress REST API post collections, including feeds served with the wrong content type.
     27* **Feed discovery.** It lists the feeds a page offers: alternates, `rel=feed` links, h-feeds and the WordPress REST API.
     28
     29On PHP 8.4 and later, HTML is parsed with PHP's own HTML5 parser; on older PHP, with the bundled masterminds/html5 parser.
     30
     31= Using it from PHP =
     32
     33    $parse = new ParseThis\Parser( 'https://example.com/a-post/' );
     34    $result = $parse->fetch();
     35    if ( ! is_wp_error( $result ) ) {
     36        $parse->parse( array( 'return' => 'single' ) );
     37        $jf2 = $parse->get();          // jf2 array.
     38        $mf2 = $parse->get( 'mf2' );   // Or the same result as mf2.
     39    }
     40
     41`ParseThis\Parser::parse()` accepts these arguments:
     42
     43* `return`: `single` for one item (default) or `feed` for a list of items.
     44* `follow`: fetch and parse external author pages. Default false.
     45* `limit`: maximum number of feed items. Default 150.
     46* `jsonld`: try JSON-LD. Default true.
     47* `html`: fall back to meta tags. Default true.
     48* `references`: move nested objects with a URL (citations, cards, events, ...) into `refs`, as the jf2 spec describes. Default true.
     49* `location`: flatten a nested location into `latitude`, `longitude` and `altitude` properties, with `location` as a plain string. Default false.
     50* `alternate`: use a `rel=alternate` jf2 or mf2 version of the page if it has one. Default false.
     51* `require_content`: whether a summary alone isn't enough, so the page's WordPress REST API version is fetched for full content. Default: true for feeds, false otherwise.
     52* `always_arrays`: always return `category`, `photo`, `video`, `audio`, `syndication`, `like-of`, `repost-of`, `bookmark-of` and `in-reply-to` as arrays, as Microsub does. Default false, which follows jf2: a single value is not wrapped in an array.
     53* `debug`: include the raw data each source was read from (`_meta`, `_jsonld`, `_json`, `_yt`, `_ombed`, `_rest`). Default false. Before 2.0.0 this was added whenever `WP_DEBUG` was on.
     54
     55To list a page's feeds instead, use `( new ParseThis\Discovery() )->fetch( $url )`.
     56
     57= Output format =
     58
     59Results follow [jf2](https://jf2.spec.indieweb.org/), with a few deliberate differences:
     60
     61* A feed's entries are in `items`, as in Microsub timelines, rather than the JF2 Feed profile's `children`.
     62* Nested objects with a URL (citations, cards, events, products and any other type) are stored in `refs`, keyed by URL, as XRay does, rather than jf2's `references`. The property holds the URL. Authors stay cards.
     63* Microformats properties that Parse This doesn't know are passed through as they are.
     64* Entries get a `post-type` property from [Post Type Discovery](https://www.w3.org/TR/post-type-discovery/).
     65* Reviews can be an h-review, or an h-entry with `review-of` (a URL, or a nested h-cite, h-card, h-event, h-item or h-product) and `rating`, `best` and `worst`, as proposed in [microformats/h-entry#32](https://github.com/microformats/h-entry/issues/32) and published by Post Kinds. Both, and an h-entry that is also an h-review, get the post type `review`.
     66* `author` is always a card (or a list of cards), never a plain string.
     67* Properties starting with an underscore (`_links`, `_rest`, ...) are internal or debugging data.
     68* A fetched result has `_code`, the HTTP status (or the status the page declares with `<meta http-equiv="Status">`, such as 410 for a deleted post's stub), and `_source_format`, what it was read from: `mf2+html` (microformats), `html` (meta tags only), `mf2+json`, `jf2+json`, `feed+json` (JSON Feed), `xml` (RSS or Atom) or `wordpress` (REST API). After redirects, the document is read from, and relative links resolve against, the address it ended up at.
     69* When a URL can't be parsed, `fetch()` returns a `WP_Error`: `invalid-url`, `not_found` (404), `unauthorized` (401), `forbidden` (403), `http_error` (any other 4xx or 5xx, with the status as `response_code` in the error data), `content-type` (a format Parse This doesn't read), or the request error itself (a timeout, for example).
     70
     71Values from fetched pages are sanitized: URL properties only contain `http` and `https` URLs, plain-text properties (`name`, `summary`, `category`, `content`'s `text`) have no HTML tags, and `content`'s `html` is limited to a safe set of tags. Escape values when you output them all the same, as with any remote data. Debugging data (`_jsonld`, `_meta`, ...) is not sanitized.
     72
     73= REST API =
     74
     75`GET /wp-json/parse-this/1.0/parse?url=https://example.com/`
     76
     77* `url` (required): the URL to parse.
     78* `return`, `follow`, `references`, `location`, `require_content`, `always_arrays`, `debug`: as for `parse()` above.
     79* `mf2`: return mf2 instead of jf2.
     80* `discovery`: list the URL's feeds instead of parsing it.
     81* `nocache`: fetch the URL again instead of using a cached result.
     82
     83Results are cached for 15 minutes per URL and set of parameters, so pasting the same link again doesn't fetch it again. Requests with `debug` are never cached.
     84
     85The endpoint requires the `edit_posts` capability (Contributors and above); the `parse_this_rest_capability` filter changes it. Anyone who can use it can also try it from **Tools > Parse This**.
     86
     87= Filters =
     88
     89* `pt_rewrite_secure`: the list of domains whose `http://` URLs are upgraded to `https://` before fetching.
     90* `parse_this_img_filters`: an image URL found in a page, after the built-in exclusions (ads, spinners, tracking pixels and so on). Return an empty string to drop it.
     91* `parse_this_url_shorteners`: the hosts treated as link shorteners. Links to them in a summary are expanded to where they redirect; other links are left as they are, without a request.
     92* `parse_this_native_html_parser`: whether to parse HTML with PHP's own HTML5 parser on PHP 8.4 and later. Default true. Return false to use the bundled masterminds/html5 parser instead; the two only differ on invalid markup, where PHP's parser does what browsers do.
     93* `parse_this_max_requests`: how many further requests one parse may make for followed author pages and short-link expansion. Default 10. Receives the URL.
     94* `parse_this_cache_lifetime`: how long REST endpoint results are cached, in seconds. Default 15 minutes. Return 0 to turn caching off. Receives the URL.
     95* `parse_this_rest_api_jf2_type`: the jf2 type for a post read through a site's WordPress REST API. Default `entry`. Receives the REST API post object, which includes its WordPress post type, and the site's REST API root URL.
     96
     97= Helper functions =
     98
     99All classes and functions are in the `ParseThis` namespace.
     100
     101* `ParseThis\mf2_to_jf2()` and `ParseThis\jf2_to_mf2()` convert between the two formats.
     102* `ParseThis\post_type_discovery()` returns the IndieWeb post type (note, article, reply, like, photo, ...) of a jf2 or mf2 entry.
     103* `ParseThis\jf2_references()` and `ParseThis\jf2_location()` apply the `references` and `location` transformations to any jf2 object.
     104
     105Every function and class is documented in the source.
     106
     107= Upgrading from 1.x =
     108
     109Version 2.0.0 moved everything into the `ParseThis` namespace and dropped the `Parse_This_` prefix from class names (for example `Parse_This_MF2` is now `ParseThis\MF2`, and `Parse_This` is `ParseThis\Parser`). The old names that Post Kinds and Yarns use still work for now, as deprecated aliases:
     110
     111* Classes: `Parse_This`, `Parse_This_Discovery`, `Parse_This_MF2`, `Parse_This_MF2_Utils` and `REST_Parse_This`.
     112* Functions: `mf2_to_jf2()`, `jf2_to_mf2()`, `post_type_discovery()`, `pt_load_domdocument()` and `seconds_to_iso8601()`.
     113
     114Other old global names are no longer defined. Please switch to the namespaced names.
     115
     116= Using Parse This from another plugin =
     117
     118The recommended way is to require the Parse This plugin rather than bundle a copy. On WordPress 6.5 and later, add `Requires Plugins: parse-this` to your plugin's header. On older WordPress and ClassicPress, which don't read that header, check that Parse This is loaded and new enough before using it:
     119
     120    add_action( 'plugins_loaded', function () {
     121        if ( ! defined( 'PARSE_THIS_VERSION' ) || version_compare( PARSE_THIS_VERSION, '2.0.0', '<' ) ) {
     122            // Show a notice asking for Parse This 2.0.0 or later, and skip the features that need it.
     123            return;
     124        }
     125        // Use ParseThis\Parser and the rest here.
     126    }, 11 );
     127
     128`PARSE_THIS_VERSION` is the version of the copy of Parse This that loaded. It isn't defined by copies older than 2.0.0, so an older copy bundled in another plugin shows up as missing. The `parse_this_loaded` action fires, with the version, once Parse This has loaded.
     129
     130= Bundling Parse This in another plugin =
     131
     132If you do bundle it, copy the plugin into your plugin (for example under `lib/parse-this/`) and load it only if the standalone plugin hasn't already, from `plugins_loaded` at priority 10 or later:
     133
     134    add_action( 'plugins_loaded', function () {
     135        if ( ! function_exists( 'parse_this_loader' ) ) {
     136            require_once __DIR__ . '/lib/parse-this/parse-this.php';
     137            parse_this_loader();
     138        }
     139    }, 11 );
     140
     141The standalone plugin loads at priority 9, so when it is active it is used instead of your copy. Every function is also wrapped in `function_exists()` and classes are loaded by an autoloader, so if two copies do load, the first one wins.
     142
     143Copies of Parse This 1.x bundled in other plugins keep working alongside 2.0: when the standalone plugin is active, their calls to the old names listed under "Upgrading from 1.x" are served by the standalone plugin.
    26144
    27145== Frequently Asked Questions ==
    28146
     147= What is jf2? =
     148
     149jf2 is a JSON format derived from microformats2, but simpler: single values aren't wrapped in arrays, and types drop the `h-` prefix. See the [jf2 specification](https://jf2.spec.indieweb.org/).
     150
     151= Why does a page return only a title and a URL? =
     152
     153That page has no microformats, JSON-LD or useful meta tags, or it blocks automated requests. The **Tools > Parse This** page shows exactly what was parsed for a URL.
     154
     155= Which copy is used when several plugins bundle Parse This? =
     156
     157The first one loaded. If the standalone plugin is active, it is that one. See "Bundling Parse This in another plugin" above.
     158
     159= Does it work with ClassicPress? =
     160
     161Yes. It is tested with ClassicPress 2.7 on PHP 7.4 to 8.3.
     162
    29163== Changelog ==
     164
     165= 2.0.0 ( 2026-10-03 ) =
     166
     167A major update. Please read "Upgrading from 1.x" if another plugin of yours uses Parse This.
     168
     169Requirements:
     170
     171* Requires PHP 7.4 and WordPress 6.2, or ClassicPress 2.x. Tested up to WordPress 7.1 and PHP 8.5.
     172
     173Changes to be aware of:
     174
     175* Everything is in the `ParseThis` namespace, and class names drop the `Parse_This_` prefix (`Parse_This` is now `ParseThis\Parser`). The old names Post Kinds and Yarns use still work, as deprecated aliases.
     176* Output: nested objects with a URL move to `refs`; authors are always cards; RSS and Atom items use `post-type` (not `post_type`); unknown microformats properties are kept; and fetched results carry `_code` and `_source_format`.
     177* `fetch()` returns a `WP_Error` for HTTP error pages (`not_found`, `unauthorized`, `forbidden`, `http_error`) instead of parsing them; a 410 Gone page is still parsed.
     178* The REST endpoint and the Tools > Parse This page require the `edit_posts` capability (filterable with `parse_this_rest_capability`), and its parameters are validated.
     179* Raw source data (`_meta`, `_jsonld` and so on) is only included with the new `debug` argument, not whenever `WP_DEBUG` is on.
     180* Plugins that use Parse This should require it rather than bundle it: see "Using Parse This from another plugin". `PARSE_THIS_VERSION` and the `parse_this_loaded` action help with that.
     181* The Instagram parser and the `ifset()` helper are removed.
     182
     183New:
     184
     185* Microformats always come first; JSON-LD, meta tags and the REST API only fill gaps. The `require_content` argument controls when a summary is enough.
     186* Microformats: choose the item a busy page is about; the full authorship algorithm, with feed entries inheriting the feed's author; reviews as an h-entry with `review-of`, `rating`, `best` and `worst` (as Post Kinds publishes them); complete h-event, h-review and h-recipe parsing; `follow-of`; and every value of repeated properties.
     187* Links with a fragment parse just that element; WordPress comment links fall back to the site's REST API.
     188* JSON-LD: reviews, products, recipes, and Article and Event subtypes, preferred over the WebPage node SEO plugins add.
     189* Feeds and JSON served with the wrong content type are recognized. JSON Feed items get plain text, a `post-type` and absolute URLs.
     190* Discovery finds mf2 JSON alternates, `rel=feed` links, RSS 1.0, multi-value `rel` attributes and JSON Feeds advertised as `application/json`, and follows permanent redirects.
     191* PHP's own HTML5 parser is used on PHP 8.4 and later: several times faster, and it handles invalid markup as browsers do (`parse_this_native_html_parser` turns it off).
     192* The `always_arrays` argument returns Microsub-style arrays.
     193* New filters: `parse_this_rest_api_jf2_type`, `parse_this_url_shorteners`, `parse_this_max_requests`, `parse_this_cache_lifetime`, `parse_this_rest_capability` and `parse_this_native_html_parser`.
     194
     195Fixes:
     196
     197* More than 50 bugs, many of them fatal errors on PHP 8, across RSS and Atom, the WordPress REST API, JSON-LD, meta tags, microformats, JSON Feed, X (Twitter), YouTube and feed discovery.
     198* Content HTML no longer loses the text before its first tag, and escaped text (`&lt;code&gt;`) no longer turns into markup.
     199* REST API posts are dated from their GMT dates; YouTube pages are read in full; titles that only repeat the content are dropped; names and ratings of "0" are kept; post types follow Post Type Discovery.
     200
     201Performance:
     202
     203* Feeds are downloaded once rather than twice; only link-shortener URLs in summaries are expanded; REST API tags and site details need far fewer requests; followed author pages are fetched once per request; REST endpoint results are cached for 15 minutes (`nocache` and `parse_this_cache_lifetime`).
     204
     205Security:
     206
     207* Output from fetched pages is sanitized (URLs limited to http and https, tags stripped from plain text, a safe set of HTML tags in content); one parse makes at most 10 further requests; the debug page sends its nonce in a header; plugin files exit when loaded outside WordPress; the OPML class handles invalid input safely.
     208
     209Developers:
     210
     211* Core's `fetch_feed()` is used for RSS and Atom; all requests go through `ParseThis\pt_remote_get()`.
     212* The bundled php-mf2 (0.5.0) and masterminds/html5 (2.11.0) are updated, and polyfills WordPress 6.2 provides are removed.
     213* Every function, class and hook is documented, and the plugin is tested on WordPress 6.2, the latest WordPress and ClassicPress 2.7 with PHP 7.4 to 8.5.
    30214
    31215= 1.0.1 ( 2021-04-02 ) =
    … …  
    35219= 1.0.0 ( 2020-12-15 ) =
    36220* First Official Release. Prior to this point it was in a point release.
     221
     222== Upgrade Notice ==
     223
     224= 2.0.0 =
     225A major update: classes and functions move to the ParseThis namespace (the old names Post Kinds and Yarns use still work), the output format changes in places, and the REST endpoint now requires the edit_posts capability. Requires PHP 7.4 and WordPress 6.2.
  • indieweb-post-kinds/tags/4.0.0/readme.txt

    r3067251 r3738687  
    22Contributors: dshanske
    33Tags: indieweb, interaction, posts, webmention, share, like, scrobble
    4 Stable tag: 3.7.3
    5 Requires at least: 4.9.9
    6 Requires PHP: 7.0
    7 Tested up to: 6.5
     4Stable tag: 4.0.0
     5Requires at least: 6.2
     6Requires PHP: 7.4
     7Tested up to: 7.1
    88License: GPLv2 or later
    99License URI: http://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2121Many sites will not need all of the kinds set up. What kinds of posts you wish to publish are specific to your needs.
    2222
     23Post Kinds requires PHP 7.4 or later and WordPress 6.2 or later. It also supports ClassicPress 2.7.3 or later.
     24
    2325
    2426== Screenshots ==
    … …  
    4244
    4345== Upgrade Notice ==
     46
     47= 4.0.0 =
     48
     49Requires PHP 7.4 and WordPress 6.2 or ClassicPress 2.7.3. Response data is now escaped, changing some display helpers; stored post data upgrades in the background. Posts from apps and the REST API now get the Default Kind (Note unless changed), not Article. See the changelog.
    4450
    4551= 3.7.0 =
    … …  
    118124either a response to something, such as a URL, or a more passive type of post where you are recording/logging something you did, for
    119125example, watched a movie.
     126
     127When you write a post in the Classic Editor, choose its kind in the Kind box. For kinds that respond to something, fill in the Response Properties box:
     128
     129 * **URL** - the page you are responding to. When you leave this field, the plugin fetches the page and fills in the other fields from it where it can. You can edit or clear anything it fills in.
     130 * **Name** - the title of what you are responding to.
     131 * **RSVP** and **Rating** - shown only for the kinds that use them: RSVP for RSVPs, and a rating for reviews, listens, watches, reads, plays and similar.
     132 * **Details** - more about what you are responding to: a summary, quote or caption (plain text), the site name, publication or album, when it was published and updated, its tags (separated by semicolons) and a featured image URL. For listens, watches, photos and similar, it also has the duration, or a start and end time.
     133 * **Author** - the name, URL and photo of the author or artist. Separate several authors with semicolons.
     134 * **Upload or Attach Media** - for photo, video and audio posts, attaches a file from the media library.
     135 * **Clear** - empties every field.
     136
     137The plugin uses these fields to display the response above your post's content, marked up with [microformats](https://microformats.org/wiki/h-entry) so other sites can read it.
    120138
    121139= How do I interact with other sites? =
    … …  
    203221However if you are interested in creating your own there is functionality around it.
    204222
    205 register_post_kind(
    206         'reply',
     223        register_post_kind(
     224                'reply',
    207225                array(
    208                         'singular_name'   => __( 'Reply', 'indieweb-post-kinds' ), // Name for one instance of the kind
    209                         'name'            => __( 'Replies', 'indieweb-post-kinds' ), // General name for the kind plural
    210                         'verb'            => __( 'Replied to', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    211                         'property'        => 'in-reply-to', // microformats 2 property
    212                         'format'          => 'link', // Post Format that maps to this
    213                         'description'     => __( 'a reply to content typically on another site', 'indieweb-post-kinds' ),
    214                         'description-url' => 'http://indieweb.org/reply',
    215                         'title'           => false, // Should this kind have an explicit title
    216                         'show'            => true, // Show in Settings
    217                         )
     226                        'singular_name'   => __( 'Reply', 'indieweb-post-kinds' ), // Name for one instance of the kind.
     227                        'name'            => __( 'Replies', 'indieweb-post-kinds' ), // General name for the kind, plural.
     228                        'verb'            => __( 'Replied to', 'indieweb-post-kinds' ), // The verb or action, such as "Liked".
     229                        'property'        => 'in-reply-to', // Microformats 2 property.
     230                        'format'          => 'link', // Post format that maps to this kind.
     231                        'description'     => __( 'a reply to content typically on another site', 'indieweb-post-kinds' ),
     232                        'description_url' => 'http://indieweb.org/reply',
     233                        'title'           => false, // Whether this kind should have an explicit title.
     234                        'show'            => true, // Whether to show the kind in Settings.
    218235                )
    219 );
    220 
    221 Add a function with your kind in the above format, hooking it in the init hook and it will add the Kind to the system.
     236        );
     237
     238Call it from a function hooked to `init` and it will add the Kind to the system. The [developer notes](https://github.com/dshanske/indieweb-post-kinds/blob/trunk/develop.txt) list the other arguments.
    222239
    223240= Can I enable one of the Kinds you plan to offer in future? =
    … …  
    225242`set_post_kind_visibility( $slug, $show = true )` - If you add this function in early on, it will change the visibility of a kind.
    226243
    227 = Can I create archives for each kind? ==
     244= Can I create archives for each kind? =
    228245
    229246Post Kinds automatically handles the display of archives of individual types. So to view all the posts marked as "note", for example, one could visit the URL http://www.YOURSITE.COM/kind/note/.
    230247Simply replace YOURSITE.COM with your particular site name and the particular post kind name to access the others.
    231248
     249To use another word than "kind" in these URLs, for example in your own language, set the Kind base on Settings > Permalinks. Links to the old URLs then stop working, as when the category base changes.
     250
    232251You can also add the date /kind/note/2018/12/24 to see date-based archives.
    233252
    234 For archives if you add exclude_kind as a query variable it will exclude specific kinds from the query `?exclude=kind&exclude_terms=note`. You can also do this as /exclude/kind/note,checkin as it accepts multiple values
    235 
    236 There is also a special photo photo included, using ?kind_photos=1 or /photos or /photos/yyyy or /photos/yyyy/mm or /photos/kind/note or any other taxonomy. This will use the photo enhancements introduced in 3.4.0 to only show photos from
     253To leave specific kinds out of an archive, use `?exclude=kind&exclude_terms=note` or /exclude/kind/note,checkin, which accepts several kinds.
     254
     255There is also a special photo archive, using ?kind_photos=1 or /photos or /photos/yyyy or /photos/yyyy/mm or /photos/kind/note or any other taxonomy. This will use the photo enhancements introduced in 3.4.0 to only show photos from
    237256all types of posts.
    238257
    … …  
    270289== Theme Support ==
    271290
    272 Post Kinds automatically adds information to `the_content` and `the_excerpt` filter. Being as this is inside the content block, which may or may not be desirable, you may remove these filters as noted
    273 below and call `kind_display` directly. This will allow it to appear outside the content block. To remove the automatic display, add the following to your theme.
    274         * `add_filter( 'kind_content_display', '__return_false' );`
    275 
    276 The functions `has_post_kind`, `set_post_kind`, and `set_post_kind` will allow you to manipulate the kind settings in a post. `get_post_kind_string` will return the display name of a kind.
    277 
    278 If you want to customize the look of the display, you can create a directory in your theme called `kind_views`, copy the file from the views directory of the plugin, and modify it. This will persist
    279 through future plugin updates.
     291Post Kinds works with most themes without changes. It adds the response (what you replied to, liked, listened to and so on) to the post's content and excerpt, and to feeds.
     292
     293To place the response somewhere else in your theme, turn off the automatic display and call `kind_display()` in your template where you want it:
     294
     295        add_filter( 'kind_content_display', '__return_false' );
     296
     297Many kinds have no title. In templates that list posts, `kind_get_the_title()` returns the post's title, or generates one from the kind and what it responds to.
     298
     299To change how a kind looks, create a `kind_views` folder in your theme, copy the view from the plugin's `views` folder into it, and edit the copy. It persists through plugin updates, but it doesn't receive fixes: compare it with the plugin's views after upgrading.
     300
     301To style responses, target the `.response` wrapper that each view uses, and `.kind-embed` around embedded content.
     302
     303The template functions, hooks and view variables are listed in the [developer notes](https://github.com/dshanske/indieweb-post-kinds/blob/trunk/develop.txt).
    280304
    281305
    282306== Changelog ==
    283307
    284 * 3.7.3 ( 2024-04-09 ) =
     308= 4.0.0 ( 2026-10-10 ) =
     309* Raise the minimum requirements to PHP 7.4 and WordPress 6.2, and declare ClassicPress 2.7.3 support
     310* The `kind` field in the REST API now has a description and lists the valid kinds; an unknown kind is rejected with `rest_invalid_param`
     311* When the old IndieWeb Taxonomy plugin is active, activation now stops with a translatable message and deactivates Post Kinds itself, instead of a file that isn't a plugin
     312* The Default Kind setting applies to every new post without a kind, including posts from apps, the REST API, Quick Draft and scheduled drafts, which used to get Article. Set Default Kind to Article to keep the old behaviour. A translated site no longer gets a stray copy of the Article kind
     313* Escape response data in views, the editor, widgets and settings; summaries are displayed as plain text
     314* Sanitize input from the editor, the media modal's artist fields, settings and widgets; citation summaries are saved as plain text
     315* An RSVP can now be cleared in the editor
     316* Fix the Response Properties box disappearing for every kind after another box was collapsed while editing a note or article; it can no longer be hidden through Screen Options, since the editor shows it only for kinds that use it
     317* Kind icons can be resized from a theme with any rule, such as `.svg-icon { width: 1rem; height: 1rem; }`. The inline style is gone, the plugin's styles have no specificity, and icons have a default size where the stylesheet doesn't load, such as feed readers
     318* Deleting the plugin removes its settings and scheduled events, on every site of a multisite network. Posts, their kinds and their response data are kept
     319* Several authors are saved as one h-card each, pairing the semicolon-separated names, URLs and photos by position, and every author is displayed. Lists of authors from Micropub or link previews are shown and filled in too. Authors saved by earlier versions are read the same way. An author with a URL but no photo is linked, and clearing the author fields removes the authors
     320* Add a Kind base setting to Settings > Permalinks, to change the `kind` in kind archive URLs such as /kind/like/, and the `kind_base_slug` filter
     321* Fix durations calculated from the start and end times never being shown; `calculate_duration()` now also accepts date objects
     322* Require the edit_posts capability for the post-kinds/1.0/fields REST route; an unknown kind returns a 404
     323* Add a background upgrade that moves and repairs stored citations and records the media in each post; `wp post-kinds upgrade` runs it on demand (`--dry-run`, `--post=<id>`)
     324* Fix citations losing their type when viewed (a checkin lost its h-card, for example) and nesting inside themselves when saved again; the upgrade repairs existing posts
     325* Fix old citations stored as a plain URL being lost when viewed, and a PHP 8 fatal error when MF2_Post read very old response data
     326* Reading a post no longer writes to the database or downloads remote photos
     327* Performance: the media scan on save covers posts only and reads the content once; attachment lookups are cached; the photos archive and the home page kind filter use simpler queries; the settings page only writes kind terms that changed; icons, time zone offsets and permalink kinds are cached; the media fragment script loads only where there is audio or video, deferred
     328* The Kind Post widget prints nothing when it has no posts, instead of an unclosed wrapper
     329* New kind terms are named after the kind rather than its slug
     330* Feed change: The RSS and Atom feeds use WordPress's own templates instead of copies, and other plugins' feed handlers are no longer removed. Posts without a title have an empty title element instead of none, so the Atom feed is valid; turn on "Automatically add the Kind to the Title" to fill it in. Dates are written in UTC. In comment feeds, comments on untitled posts name the post.
     331* Deprecated: The global functions `display_formatted_datetime()`, `divide_datetime()`, `build_datetime()`, `get_datetime_offset()`, `divide_interval()` and `build_interval()`. Use the `Kind_Time` methods of the same names. The global names still work but report a deprecation notice when debugging is on, and they will be removed in the next major release (5.0.0). Custom views copied into a theme may call them, so update those to the `Kind_Time` methods.
     332* Display change: Custom views copied into a theme's `kind_views` folder keep their old code. Copy them again from the plugin's `views` folder, or escape their output.
     333* Display change: Citation summaries are plain text, marked up as `p-summary` instead of `e-summary`. `Kind_View::get_summary()` returns the summary markup.
     334* Display change: `Kind_View::get_hcard()`, `get_cite_title()`, `get_site_name()`, `get_embed()`, `get_url_link()` and `display_duration()` return escaped markup; HTML passed as a name is shown as text.
     335* Display change: `kind_get_the_title()` escapes the content before the `kind_get_the_title_content` filter.
     336* Display change: `Kind_View::rsvp_text()` takes optional URL and name arguments and returns the full sentence. Called with only a type, it still returns a printf-ready string. The RSVP strings have changed, so translations need updating.
     337* Display change: `Kind_View::rating_text()` ignores non-numeric ratings and shows at most 10 stars.
     338* Upgrade change: Published posts with no kind are given one, based on their stored response properties, or Article. If the home page shows only selected kinds, Article is added to them so those posts stay; posts given another kind, such as Like, follow the selection.
     339* Upgrade change: Until the background upgrade reaches them, older posts may be missing from the photos archive; `wp post-kinds upgrade` finishes it at once.
     340* Removed: The `kind_photo_shortcode_exclude` filter, which never ran.
     341* Behaviour change: `MF2_Post::get_images()` no longer imports remote photos into the media library; it returns media library attachments only.
     342* Deprecated: The `MF2_Post` class. Use `Kind_Post`. Creating an `MF2_Post` reports a deprecation notice when debugging is on; the class will be removed in a future major version.
     343
     344= 3.7.3 ( 2024-04-09 ) =
    285345* Ensure widget array is instantiated
    286346
  • indieweb-post-kinds/tags/4.0.0/templates/reply-author.php

    r2399112 r3738687  
     1<?php
     2/**
     3 * Author tab of the Post Kinds metabox.
     4 *
     5 * Included by reply-metabox.php.
     6 *
     7 * @package Post_Kinds
     8 * @since   3.0.0
     9 *
     10 * @var array $cite The citation, normalized by Kind_Post::normalize_cite().
     11 */
     12
     13defined( 'ABSPATH' ) || exit;
     14?>
    115        <div id="kind-author" class="hide-if-js">
    2         <h4> <?php _e( 'Information on the Author or Artist of the Piece', 'indieweb-post-kinds' ); ?></h4>
    3                 <?php _e( '(Multiple Entries separated by semicolon)', 'indieweb-post-kinds' ); ?><BR />
     16        <h4> <?php esc_html_e( 'Information on the Author or Artist of the Piece', 'indieweb-post-kinds' ); ?></h4>
     17                <?php esc_html_e( '(Multiple Entries separated by semicolon)', 'indieweb-post-kinds' ); ?><BR />
    418        <p class="field-row">
    519        <label for="cite_author_name" class="three-quarters">
    6                 <?php _e( 'Author', 'indieweb-post-kinds' ); ?>
    7                         <input type="text" name="cite_author_name" id="cite_author_name" class="widefat" value="<?php echo $cite['author']['name']; ?>" />
     20                <?php esc_html_e( 'Author', 'indieweb-post-kinds' ); ?>
     21                        <input type="text" name="cite_author_name" id="cite_author_name" class="widefat" value="<?php echo esc_attr( $cite['author']['name'] ); ?>" />
    822        </label>
    923        </p>
    1024        <p class="field-row">
    1125        <label for="cite_author_url" class="three-quarters">
    12                 <?php _e( 'Author URL', 'indieweb-post-kinds' ); ?>
    13                         <input type="text" name="cite_author_url" id="cite_author_url" class="widefat" value="<?php echo $cite['author']['url']; ?>" />
     26                <?php esc_html_e( 'Author URL', 'indieweb-post-kinds' ); ?>
     27                        <input type="text" name="cite_author_url" id="cite_author_url" class="widefat" value="<?php echo esc_attr( $cite['author']['url'] ); ?>" />
    1428        </label>
    1529        </p>
    1630        <p class="field-row">
    1731        <label for="cite_author_photo" class="three-quarters">
    18                 <?php _e( 'Author Photo URL', 'indieweb-post-kinds' ); ?>
    19                         <input type="text" name="cite_author_photo" id="cite_author_photo" class="widefat" value="<?php echo $cite['author']['photo']; ?>" />
     32                <?php esc_html_e( 'Author Photo URL', 'indieweb-post-kinds' ); ?>
     33                        <input type="text" name="cite_author_photo" id="cite_author_photo" class="widefat" value="<?php echo esc_attr( $cite['author']['photo'] ); ?>" />
    2034        </label>
    2135        </p>
  • indieweb-post-kinds/tags/4.0.0/templates/reply-details.php

    r2399112 r3738687  
     1<?php
     2/**
     3 * Details tab of the Post Kinds metabox.
     4 *
     5 * Included by reply-metabox.php; includes reply-time.php.
     6 *
     7 * @package Post_Kinds
     8 * @since   3.0.0
     9 *
     10 * @var Kind_Post $kind_post The post being edited.
     11 * @var array     $cite      The citation, normalized by Kind_Post::normalize_cite().
     12 */
     13
     14defined( 'ABSPATH' ) || exit;
     15?>
    116<div id="kind-details" class="hide-if-js">
    2         <h4><?php _e( 'Information on what you are responding to', 'indieweb-post-kinds' ); ?></h4>
     17                <h4><?php esc_html_e( 'Information on what you are responding to', 'indieweb-post-kinds' ); ?></h4>
    318        <label for="cite_summary">
    4                 <?php _e( 'Summary/Quote/Caption', 'indieweb-post-kinds' ); ?>
    5         <textarea name="cite_summary" id="cite_summary" data-role="none" class="widefat"><?php echo $cite['summary']; ?></textarea>
     19                <?php esc_html_e( 'Summary/Quote/Caption', 'indieweb-post-kinds' ); ?>
     20        <textarea name="cite_summary" id="cite_summary" data-role="none" class="widefat"><?php echo esc_textarea( html_entity_decode( wp_strip_all_tags( (string) $cite['summary'] ), ENT_QUOTES, get_bloginfo( 'charset' ) ) ); ?></textarea>
    621        </label>
    722        <p class="field-row">
    823        <label for="cite_publication" class="three-quarters">
    9                 <?php _e( 'Site Name/Publication/Album', 'indieweb-post-kinds' ); ?>
    10                         <input type="text" name="cite_publication" id="cite_publication" class="widefat" value="<?php echo $cite['publication']; ?>" />
     24                <?php esc_html_e( 'Site Name/Publication/Album', 'indieweb-post-kinds' ); ?>
     25                        <input type="text" name="cite_publication" id="cite_publication" class="widefat" value="<?php echo esc_attr( $cite['publication'] ); ?>" />
    1126        </label>
    1227        </p>
    1328        <p class="field-row">
    14                         <?php echo Kind_Metabox::kind_the_time( 'cite_published', __( 'Published/Released', 'indieweb-post-kinds' ), $cite['published'], 'published' ); ?>
    15                 <?php echo Kind_Metabox::kind_the_time( 'cite_updated', __( 'Updated', 'indieweb-post-kinds' ), $cite['updated'], 'updated' ); ?>
     29                        <?php echo Kind_Metabox::kind_the_time( 'cite_published', __( 'Published/Released', 'indieweb-post-kinds' ), $cite['published'], 'published' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::kind_the_time() escapes its output. ?>
     30                <?php echo Kind_Metabox::kind_the_time( 'cite_updated', __( 'Updated', 'indieweb-post-kinds' ), $cite['updated'], 'updated' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::kind_the_time() escapes its output. ?>
    1631        </p>
    1732        <label for="cite_tags">
    18                 <?php _e( 'Tags (semicolon separated)', 'indieweb-post-kinds' ); ?>
    19         <textarea name="cite_tags" id="cite_tags" data-role="none" class="widefat"><?php echo $cite['category'] ?></textarea>
     33                <?php esc_html_e( 'Tags (semicolon separated)', 'indieweb-post-kinds' ); ?>
     34        <textarea name="cite_tags" id="cite_tags" data-role="none" class="widefat"><?php echo esc_textarea( $cite['category'] ); ?></textarea>
    2035        </label>
    2136        <p class="field-row">
    2237        <label for="cite_featured" class="three-quarters">
    23                 <?php _e( 'Featured Image', 'indieweb-post-kinds' ); ?>
    24                         <input type="text" name="cite_featured" id="cite_featured" class="widefat" value="<?php echo $cite['featured']; ?>" />
     38                <?php esc_html_e( 'Featured Image', 'indieweb-post-kinds' ); ?>
     39                        <input type="text" name="cite_featured" id="cite_featured" class="widefat" value="<?php echo esc_attr( $cite['featured'] ); ?>" />
    2540        </label>
    2641        </p>
  • indieweb-post-kinds/tags/4.0.0/templates/reply-metabox.php

    r3001401 r3738687  
    11<?php
     2/**
     3 * Post Kinds metabox for the Classic Editor.
     4 *
     5 * Loaded by Kind_Metabox::reply_metabox(). It sets up $kind_post, $kind, $type
     6 * and $cite, then includes reply-details.php and reply-author.php, which use them.
     7 *
     8 * @package Post_Kinds
     9 * @since   3.0.0
     10 */
     11
     12defined( 'ABSPATH' ) || exit;
     13
    214wp_nonce_field( 'replykind_metabox', 'replykind_metabox_nonce' );
    315$kind_post = new Kind_Post( get_the_ID() );
    … …  
    1022}
    1123
    12 if ( empty( $cite['url'] ) && array_key_exists( 'kindurl', $_GET ) ) {
    13         $cite['url'] = esc_url_raw( $_GET['kindurl'] );
     24// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Prefills the URL on the new post screen; nothing is saved.
     25if ( empty( $cite['url'] ) && isset( $_GET['kindurl'] ) && is_string( $_GET['kindurl'] ) ) {
     26        $cite['url'] = sanitize_url( wp_unslash( $_GET['kindurl'] ) );
    1427}
     28// phpcs:enable WordPress.Security.NonceVerification.Recommended
    1529
    1630$attachment = 0;
    … …  
    1832
    1933if ( in_array( $kind, array( 'audio', 'video', 'photo' ) ) ) {
    20         $attachment = attachment_url_to_postid( $cite['url'] );
     34        $attachment = empty( $cite['url'] ) ? 0 : Kind_Post::get_attachment_id( $cite['url'] );
    2135        if ( $attachment ) {
    2236                $attachment_post = new Kind_Post( $attachment );
    … …  
    2842
    2943?>
    30 <a href="#kind-details" class="show-kind-details button hide-if-no-js"><?php _e( 'Details', 'indieweb-post-kinds' ); ?></a>
    31 <a href="#kind-author" class="show-kind-author-details button hide-if-no-js"><?php _e( 'Author', 'indieweb-post-kinds' ); ?></a>
    32 <a id="add-kind-media" class="button hide-if-no-js hidden" href="javascript:;">Upload or Attach Media</a>
    33 <button class="clear-kindmeta-button button hide-if-no-js"><?php _e( 'Clear', 'indieweb-post-kinds' ); ?></button>
     44<a href="#kind-details" class="show-kind-details button hide-if-no-js"><?php esc_html_e( 'Details', 'indieweb-post-kinds' ); ?></a>
     45<a href="#kind-author" class="show-kind-author-details button hide-if-no-js"><?php esc_html_e( 'Author', 'indieweb-post-kinds' ); ?></a>
     46<a id="add-kind-media" class="button hide-if-no-js hidden" href="javascript:;"><?php esc_html_e( 'Upload or Attach Media', 'indieweb-post-kinds' ); ?></a>
     47<button class="clear-kindmeta-button button hide-if-no-js"><?php esc_html_e( 'Clear', 'indieweb-post-kinds' ); ?></button>
    3448<p class="field-row">
    3549        <label for="cite_url" class="three-quarters">
    36                 <?php _e( 'URL', 'indieweb-post-kinds' ); ?>
    37                         <input type="text" name="cite_url" id="cite_url" class="widefat" value="<?php echo $cite['url']; ?>" />
     50                <?php esc_html_e( 'URL', 'indieweb-post-kinds' ); ?>
     51                        <input type="text" name="cite_url" id="cite_url" class="widefat" value="<?php echo esc_attr( $cite['url'] ); ?>" />
    3852        </label>
    3953</p>
    4054<p class="field-row">
    4155        <label for="cite_name" class="three-quarters">
    42                 <?php _e( 'Name', 'indieweb-post-kinds' ); ?>
    43                         <input type="text" name="cite_name" id="cite_name" class="widefat" value="<?php echo $cite['name']; ?>" />
     56                <?php esc_html_e( 'Name', 'indieweb-post-kinds' ); ?>
     57                        <input type="text" name="cite_name" id="cite_name" class="widefat" value="<?php echo esc_attr( $cite['name'] ); ?>" />
    4458        </label>
    4559</p>
    4660<p class="field-row hide-if-js" id="rsvp-option">
    47         <?php echo Kind_Metabox::rsvp_select( $kind_post->get( 'rsvp' ) ); ?>
     61        <?php echo Kind_Metabox::rsvp_select( $kind_post->get( 'rsvp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::rsvp_select() escapes its output. ?>
    4862</p>
    4963<p class="field-row hide-if-js" id="rating-option">
    50         <?php echo Kind_Metabox::rating_select( $kind_post->get( 'rating' ) ); ?>
     64        <?php echo Kind_Metabox::rating_select( $kind_post->get( 'rating' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::rating_select() escapes its output. ?>
    5165</p>
    5266<p id="kind-media hide-if-no-js">
    … …  
    5973        } elseif ( wp_attachment_is( 'audio', $attachment ) ) {
    6074                $view = new Kind_Media_View( $attachment, 'audio' );
    61                 echo $view->get();
     75                echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    6276        } elseif ( wp_attachment_is( 'video', $attachment ) ) {
    6377                $view = new Kind_Media_View( $attachment, 'video' );
    64                 echo $view->get();
     78                echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    6579        }
    6680}
    6781?>
    6882        </div>
    69         <input type="hidden" id="cite_media" name="cite_media" value="<?php echo $attachment; ?>" >
     83        <input type="hidden" id="cite_media" name="cite_media" value="<?php echo absint( $attachment ); ?>" >
    7084</p>
    7185
  • indieweb-post-kinds/tags/4.0.0/templates/reply-time.php

    r2399112 r3738687  
    1 <?php $duration = divide_interval( $kind_post->get_duration() ); ?>
     1<?php
     2/**
     3 * Duration fields of the Post Kinds metabox.
     4 *
     5 * Included by reply-details.php.
     6 *
     7 * @package Post_Kinds
     8 * @since   3.0.0
     9 *
     10 * @var Kind_Post $kind_post The post being edited.
     11 */
     12
     13defined( 'ABSPATH' ) || exit;
     14
     15$duration = Kind_Time::divide_interval( $kind_post->get_duration() ); ?>
    216
    317<div id="kind-time" class="hide-if-js">
    4         <h4><?php _e( 'Duration', 'indieweb-post-kinds' ); ?></h4>
     18        <h4><?php esc_html_e( 'Duration', 'indieweb-post-kinds' ); ?></h4>
    519        <p class="field-row">
    620        <label for="duration" class="eighth">
    7                 <?php _e( 'Years', 'indieweb-post-kinds' ); ?>
    8                         <input type="number" name="duration_years" id="duration_years" size="3" value="<?php echo $duration['Y']; ?>" />
     21                <?php esc_html_e( 'Years', 'indieweb-post-kinds' ); ?>
     22                        <input type="number" name="duration_years" id="duration_years" size="3" value="<?php echo absint( $duration['Y'] ); ?>" />
    923        </label>
    1024        <label for="duration" class="eighth">
    11                 <?php _e( 'Months', 'indieweb-post-kinds' ); ?>
    12                         <input type="number" name="duration_months" id="duration_months" size="2" min="0" max="12" value="<?php echo $duration['M']; ?>" />
     25                <?php esc_html_e( 'Months', 'indieweb-post-kinds' ); ?>
     26                        <input type="number" name="duration_months" id="duration_months" size="2" min="0" max="12" value="<?php echo absint( $duration['M'] ); ?>" />
    1327        </label>
    1428        <label for="duration" class="eighth">
    15                 <?php _e( 'Days', 'indieweb-post-kinds' ); ?>
    16                         <input type="number" name="duration_days" id="duration_days" size="2" min="0" max="30" value="<?php echo $duration['D']; ?>" />
     29                <?php esc_html_e( 'Days', 'indieweb-post-kinds' ); ?>
     30                        <input type="number" name="duration_days" id="duration_days" size="2" min="0" max="30" value="<?php echo absint( $duration['D'] ); ?>" />
    1731        </label>
    1832        <p>
    1933        <p class="field-row">
    2034        <label for="duration" class="eighth">
    21                 <?php _e( 'Hours', 'indieweb-post-kinds' ); ?>
    22                         <input type="number" name="duration_hours" id="duration_hours" size="2" min="0" max="12" value="<?php echo $duration['H']; ?>" />
     35                <?php esc_html_e( 'Hours', 'indieweb-post-kinds' ); ?>
     36                        <input type="number" name="duration_hours" id="duration_hours" size="2" min="0" max="12" value="<?php echo absint( $duration['H'] ); ?>" />
    2337        </label>
    2438        <label for="duration" class="eighth">
    25                 <?php _e( 'Minutes', 'indieweb-post-kinds' ); ?>
    26                         <input type="number" name="duration_minutes" id="duration_minutes" size="2" min="0" max="60" value="<?php echo $duration['I']; ?>" />
     39                <?php esc_html_e( 'Minutes', 'indieweb-post-kinds' ); ?>
     40                        <input type="number" name="duration_minutes" id="duration_minutes" size="2" min="0" max="60" value="<?php echo absint( $duration['I'] ); ?>" />
    2741        </label>
    2842        <label for="duration" class="eighth">
    29                 <?php _e( 'Seconds', 'indieweb-post-kinds' ); ?>
    30                         <input type="number" name="duration_seconds" id="duration_seconds" size="2" min="0" max="60" value="<?php echo $duration['S']; ?>" />
     43                <?php esc_html_e( 'Seconds', 'indieweb-post-kinds' ); ?>
     44                        <input type="number" name="duration_seconds" id="duration_seconds" size="2" min="0" max="60" value="<?php echo absint( $duration['S'] ); ?>" />
    3145        </label>
    3246        </p>
    33         <h4> <?php _e( 'Start Time and End Time will be Used to Calculate Duration if Duration Not Set.', 'indieweb-post-kinds' ); ?> </h4>
     47        <h4> <?php esc_html_e( 'Start Time and End Time will be Used to Calculate Duration if Duration Not Set.', 'indieweb-post-kinds' ); ?> </h4>
    3448        <p class="field-row">
    35                 <?php echo Kind_Metabox::kind_the_time( 'mf2_start', __( 'Start Time', 'indieweb-post-kinds' ), $kind_post->get_datetime_property( 'start' ), 'start' ); ?>
    36                 <?php echo Kind_Metabox::kind_the_time( 'mf2_end', __( 'End Time', 'indieweb-post-kinds' ), $kind_post->get_datetime_property( 'end' ), 'end' ); ?>
    37         </p>
     49                                <?php echo Kind_Metabox::kind_the_time( 'mf2_start', __( 'Start Time', 'indieweb-post-kinds' ), $kind_post->get_datetime_property( 'start' ), 'start' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::kind_the_time() escapes its output. ?>
     50                                <?php echo Kind_Metabox::kind_the_time( 'mf2_end', __( 'End Time', 'indieweb-post-kinds' ), $kind_post->get_datetime_property( 'end' ), 'end' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::kind_the_time() escapes its output. ?>
     51                </p>
    3852</div>
  • indieweb-post-kinds/tags/4.0.0/views/kind-article.php

    r1828595 r3738687  
    11<?php
    2 /*
    3  * Article Template
     2/**
     3 * Article view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.4.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
  • indieweb-post-kinds/tags/4.0.0/views/kind-audio.php

    r2406063 r3738687  
    11<?php
    2 /*
    3  * Audio Template
     2/**
     3 * Audio view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
    6 $audios = $kind_post->get_audio();
    7 $duration = null;
     22
     23defined( 'ABSPATH' ) || exit;
     24
     25$audios      = $kind_post->get_audio();
     26$duration    = null;
    827$publication = null;
    928
    … …  
    1130        if ( 1 === count( $audios ) && 0 !== $audios[0] ) {
    1231                $audio_attachment = new Kind_Post( $audios[0] );
    13                 $cite = mf2_to_jf2( $audio_attachment->get_cite() );
     32                $cite             = \ParseThis\mf2_to_jf2( $audio_attachment->get_cite() );
    1433                if ( ! $cite ) {
    1534                        $cite = array();
    … …  
    1837                        $author = Kind_View::get_hcard( $cite['author'] );
    1938                } else {
    20                         $author    = null;
     39                        $author = null;
    2140                }
    2241                if ( array_key_exists( 'duration', $cite ) ) {
    … …  
    3049<section class="response">
    3150<header>
    32 <?php echo Kind_Taxonomy::get_before_kind( 'audio' );
     51<?php
     52echo Kind_Taxonomy::get_before_kind( 'audio' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    3353if ( isset( $cite['name'] ) ) {
    34         echo sprintf( '<span>%1s</a>', $cite['name'] );
     54        printf( '<span class="p-name">%1$s</span>', esc_html( $cite['name'] ) );
    3555}
    3656if ( $duration ) {
    37         printf( '(%1$s)', $duration );
     57        printf( '(%1$s)', $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    3858}
    3959?>
    … …  
    4262<?php
    4363if ( $embed ) {
    44         printf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
     64        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
    4565} elseif ( $audios ) {
    4666        $view = new Kind_Media_View( $audios, 'audio' );
    47         echo $view->get();
     67        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    4868}
  • indieweb-post-kinds/tags/4.0.0/views/kind-bookmark.php

    r2467905 r3738687  
    11<?php
    2 /*
    3  * Bookmark Template
     2/**
     3 * Bookmark view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1533<header>
    1634<?php
    17 echo Kind_Taxonomy::get_before_kind( 'bookmark' );
     35echo Kind_Taxonomy::get_before_kind( 'bookmark' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1836if ( ! $embed ) {
    19         if ( ! empty( $cite['url'] ) ) {
    20                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $cite['url'], $cite['name'] );
    21         } else {
    22                 echo sprintf( '<span class="p-name">%1s</span>', esc_html( $cite['name'] ) );
     37        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     38        if ( $author ) {
     39                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2340        }
    24         if ( $author ) {
    25                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    26         }
    27         if ( ! empty( $cite['publication'] ) ) {
    28                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', esc_html( $cite['publication'] ) );
     41        $site_name = Kind_View::get_site_name( $cite );
     42        if ( $site_name ) {
     43                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2944        }
    3045}
    … …  
    3348<?php
    3449if ( ! empty( $embed ) ) {
    35         echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    36 } elseif ( ! empty( $cite['summary'] ) ) {
    37         echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     50        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     51} else {
     52        echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3853}
    3954
    40 // Close Response
     55// Close Response.
    4156?>
    4257</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-checkin.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Checkin Template
     2/**
     3 * Checkin view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1432<header>
    1533<?php
    16 echo Kind_Taxonomy::get_before_kind( 'checkin' );
     34echo Kind_Taxonomy::get_before_kind( 'checkin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1735if ( ! $embed ) {
    1836        if ( empty( $cite['name'] ) ) {
    … …  
    2038        }
    2139        if ( ! empty( $url ) ) {
    22                 echo sprintf( '<a href="%1s" class="u-checkin h-card">%2s</a>', $url, $cite['name'] );
     40                printf( '<a href="%1$s" class="u-checkin h-card">%2$s</a>', esc_url( $url ), esc_html( $cite['name'] ) );
    2341        } else {
    24                 echo sprintf( '<span class="h-card p-checkin">%1s</span>', $cite['name'] );
     42                printf( '<span class="h-card p-checkin">%1$s</span>', esc_html( $cite['name'] ) );
    2543        }
    2644}
    … …  
    3048if ( $cite ) {
    3149        if ( $embed ) {
    32                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
     50                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
    3351        }
    3452}
    … …  
    3654if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    3755        $view = new Kind_Media_View( $photos, 'photo' );
    38         echo $view->get();
     56        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    3957}
    40 // Close Response
     58// Close Response.
    4159?>
    4260</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-drink.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Drink Template
     2/**
     3 * Drink view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1331<header>
    1432<?php
    15 echo Kind_Taxonomy::get_before_kind( 'drink' );
     33echo Kind_Taxonomy::get_before_kind( 'drink' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1634if ( ! $embed ) {
    17         if ( ! empty( $url ) ) {
    18                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    19         } else {
    20                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
    21         }
    22         if ( array_key_exists( 'publication', $cite ) ) {
    23                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     35        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     36        $site_name = Kind_View::get_site_name( $cite );
     37        if ( $site_name ) {
     38                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2439        }
    2540}
    … …  
    2944if ( $cite ) {
    3045        if ( $embed ) {
    31                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    32         } elseif ( array_key_exists( 'summary', $cite ) ) {
    33                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     46                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     47        } else {
     48                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3449        }
    3550}
    … …  
    3752if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    3853        $view = new Kind_Media_View( $photos, 'photo' );
    39         echo $view->get();
     54        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    4055}
    41 // Close Response
     56// Close Response.
    4257?>
    4358</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-eat.php

    r3001401 r3738687  
    11<?php
    2 /*
    3  * Eat Template
     2/**
     3 * Eat view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    927}
    1028
    11 $rating  = $kind_post->get( 'rating', true );
     29$rating = $kind_post->get( 'rating', true );
    1230?>
    1331
    … …  
    1634<header>
    1735<?php
    18 echo Kind_Taxonomy::get_before_kind( 'eat' );
     36echo Kind_Taxonomy::get_before_kind( 'eat' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1937if ( ! $embed ) {
    20         if ( ! empty( $url ) ) {
    21                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    22         } else {
    23                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
    24         }
    25         if ( ! empty( $cite['publication'] ) ) {
    26                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     38        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     39        $site_name = Kind_View::get_site_name( $cite );
     40        if ( $site_name ) {
     41                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2742        }
    2843}
    … …  
    3247if ( $cite ) {
    3348        if ( $embed ) {
    34                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    35         } elseif ( array_key_exists( 'summary', $cite ) ) {
    36                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     49                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     50        } else {
     51                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3752        }
    3853}
    3954
    4055if ( $rating ) {
    41         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
    42 } 
     56        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
     57}
    4358
    4459if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    4560        $view = new Kind_Media_View( $photos, 'photo' );
    46         echo $view->get();
     61        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    4762}
    48 // Close Response
     63// Close Response.
    4964?>
    5065</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-event.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Event Template
     2/**
     3 * Event view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   3.2.0
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725?>
    … …  
    1028<header>
    1129<?php
    12 echo Kind_Taxonomy::get_before_kind( 'event' );
    13 echo get_the_title();
     30echo Kind_Taxonomy::get_before_kind( 'event' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
     31the_title();
    1432?>
    1533</header>
    1634<?php
    1735$start = $kind_post->get( 'start' );
    18 $end = $kind_post->get( 'end' );
    19 $timestring = '<p>%1$s: <time class="%2$s" datetime="%3$s">%4$s</time></p>';
     36$end   = $kind_post->get( 'end' );
    2037
    2138if ( $start ) {
    22         printf( $timestring, __( 'Start', 'indieweb-post-kinds' ), 'dt-start', $start->format( DATE_W3C ), display_formatted_datetime( $start ) );
    23 } 
     39        printf( '<p>%1$s: <time class="dt-start" datetime="%2$s">%3$s</time></p>', esc_html__( 'Start', 'indieweb-post-kinds' ), esc_attr( $start->format( DATE_W3C ) ), esc_html( Kind_Time::display_formatted_datetime( $start ) ) );
     40}
    2441if ( $end ) {
    25         printf( $timestring, __( 'End', 'indieweb-post-kinds' ), 'dt-end', $end->format( DATE_W3C ), display_formatted_datetime( $end ) );
    26 }       
     42        printf( '<p>%1$s: <time class="dt-end" datetime="%2$s">%3$s</time></p>', esc_html__( 'End', 'indieweb-post-kinds' ), esc_attr( $end->format( DATE_W3C ) ), esc_html( Kind_Time::display_formatted_datetime( $end ) ) );
     43}
    2744if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    2845        $view = new Kind_Media_View( $photos, 'photo' );
    29         echo $view->get();
     46        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    3047}
    31 // Close Response
     48// Close Response.
    3249?>
    3350</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-favorite.php

    r2455262 r3738687  
    11<?php
    2 /*
    3  * Favorite Template
     2/**
     3 * Favorite view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1432<header>
    1533<?php
    16 echo Kind_Taxonomy::get_before_kind( 'favorite' );
     34echo Kind_Taxonomy::get_before_kind( 'favorite' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1735if ( ! $embed ) {
    18         if ( ! empty( $url ) ) {
    19                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    20         } else {
    21                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
     36        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     37        if ( $author ) {
     38                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2239        }
    23         if ( $author ) {
    24                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    25         }
    26         if ( ! empty( $cite['publication'] ) ) {
    27                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     40        $site_name = Kind_View::get_site_name( $cite );
     41        if ( $site_name ) {
     42                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2843        }
    2944}
    … …  
    3348if ( $cite ) {
    3449        if ( $embed ) {
    35                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    36         } elseif ( array_key_exists( 'summary', $cite ) ) {
    37                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     50                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     51        } else {
     52                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3853        }
    3954}
    4055
    41 // Close Response
     56// Close Response.
    4257?>
    4358</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-issue.php

    r2467905 r3738687  
    11<?php
    2 /*
    3  * Issue Template
     2/**
     3 * Issue view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1432<header>
    1533<?php
    16 echo Kind_Taxonomy::get_before_kind( 'issue' );
     34echo Kind_Taxonomy::get_before_kind( 'issue' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1735if ( ! $embed ) {
    18         if ( ! empty( $url ) ) {
    19                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    20         } else {
    21                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
     36        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     37        if ( $author ) {
     38                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2239        }
    23         if ( $author ) {
    24                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    25         }
    26         if ( ! empty( $cite['publication'] ) ) {
    27                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     40        $site_name = Kind_View::get_site_name( $cite );
     41        if ( $site_name ) {
     42                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2843        }
    2944}
    … …  
    3348if ( $cite ) {
    3449        if ( $embed ) {
    35                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    36         } elseif ( array_key_exists( 'summary', $cite ) ) {
    37                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     50                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     51        } else {
     52                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3853        }
    3954}
    4055
    41 // Close Response
     56// Close Response.
    4257?>
    4358</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-itinerary.php

    r2387393 r3738687  
    11<?php
    2 /*
    3  * Event Template
     2/**
     3 * Itinerary view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   3.2.0
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
    622
     23defined( 'ABSPATH' ) || exit;
    724
    825$itineraries = $kind_post->get( 'itinerary', false );
     26$itineraries = is_array( $itineraries ) ? $itineraries : array();
    927
    10 foreach( $itineraries as $key => $value ) {
    11         $itineraries [ $key ] = mf2_to_jf2( $value );
     28foreach ( $itineraries as $key => $value ) {
     29        $leg = \ParseThis\mf2_to_jf2( $value );
     30        // Each field is shown as text; fill in any that are missing.
     31        foreach ( array( 'operator', 'number', 'transit-type', 'origin', 'destination', 'departure', 'arrival' ) as $field ) {
     32                $leg[ $field ] = ( isset( $leg[ $field ] ) && is_scalar( $leg[ $field ] ) ) ? (string) $leg[ $field ] : '';
     33        }
     34        $itineraries[ $key ] = $leg;
    1235}
    1336
    … …  
    1841<header>
    1942<?php
    20 echo Kind_Taxonomy::get_before_kind( 'itinerary' );
    21 echo get_the_title();
     43echo Kind_Taxonomy::get_before_kind( 'itinerary' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
     44the_title();
    2245?>
    2346</header>
    2447<?php
    25 foreach( $itineraries as $itinerary ) {
    26 ?>
    27    <div class="h-leg p-itinerary">
    28                 <h3><span class="p-operator"><?php echo $itinerary['operator']; ?></span>
    29                 <span class="p-number"><?php echo $itinerary['number']; ?></span></h3>
    30                 <data class="p-transit-type" value="<?php echo $itinerary['transit-type']; ?>">
    31    <ul>
     48foreach ( $itineraries as $itinerary ) {
     49        ?>
     50        <div class="h-leg p-itinerary">
     51                <h3><span class="p-operator"><?php echo esc_html( $itinerary['operator'] ); ?></span>
     52                <span class="p-number"><?php echo esc_html( $itinerary['number'] ); ?></span></h3>
     53                <data class="p-transit-type" value="<?php echo esc_attr( $itinerary['transit-type'] ); ?>"></data>
     54        <ul>
    3255        <li>
    33                 <?php _e( 'Departs: ', 'indieweb-post-kinds' ); ?>
    34                 <span class="p-origin"><?php echo $itinerary['origin']; ?></span>
    35                 <time class="dt-departure" datetime="<?php echo $itinerary['departure']; ?>"><?php echo display_formatted_datetime( $itinerary['departure'] ); ?></time>
     56                <?php esc_html_e( 'Departs: ', 'indieweb-post-kinds' ); ?>
     57                <span class="p-origin"><?php echo esc_html( $itinerary['origin'] ); ?></span>
     58                <time class="dt-departure" datetime="<?php echo esc_attr( $itinerary['departure'] ); ?>"><?php echo esc_html( Kind_Time::display_formatted_datetime( $itinerary['departure'] ) ); ?></time>
    3659        </li>
    3760        <li>
    38                 <?php _e( 'Arrives: ', 'indieweb-post-kinds' ); ?>
    39                 <span class="p-destination"><?php echo $itinerary['destination']; ?></span>
    40                 <time class="dt-arrival" datetime="<?php echo $itinerary['arrival']; ?>"><?php echo display_formatted_datetime( $itinerary['arrival'] ); ?></time>
     61                <?php esc_html_e( 'Arrives: ', 'indieweb-post-kinds' ); ?>
     62                <span class="p-destination"><?php echo esc_html( $itinerary['destination'] ); ?></span>
     63                <time class="dt-arrival" datetime="<?php echo esc_attr( $itinerary['arrival'] ); ?>"><?php echo esc_html( Kind_Time::display_formatted_datetime( $itinerary['arrival'] ) ); ?></time>
    4164        </li>
    42    </ul>
    43    </div>
    44 <?php
     65        </ul>
     66        </div>
     67        <?php
    4568}
    4669
    4770
    48 // Close Response
     71// Close Response.
    4972?>
    5073</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-jam.php

    r2467905 r3738687  
    11<?php
    2 /*
    3   Jam Template
     2/**
     3 * Jam view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    2038<header>
    2139<?php
    22 echo Kind_Taxonomy::get_before_kind( 'jam' );
     40echo Kind_Taxonomy::get_before_kind( 'jam' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    2341if ( ! $embed ) {
    2442        if ( $title ) {
    25                 echo $title;
     43                echo $title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
    2644        }
    2745        if ( $author ) {
    28                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     46                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2947        }
    3048        if ( $site_name ) {
    31                 echo __( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>';
     49                echo esc_html__( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3250        }
    3351        if ( $duration ) {
    34                 echo Kind_View::display_duration( $duration );
     52                echo Kind_View::display_duration( $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    3553        }
    3654}
    … …  
    4058if ( $cite ) {
    4159        if ( $embed ) {
    42                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    43         } elseif ( array_key_exists( 'summary', $cite ) ) {
    44                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     60                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     61        } else {
     62                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    4563        }
    4664}
    … …  
    4866if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    4967        $view = new Kind_Media_View( $photos, 'photo' );
    50         echo $view->get();
     68        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    5169}
    52 // Close Response
     70// Close Response.
    5371?>
    5472</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-like.php

    r2467905 r3738687  
    11<?php
    2 /*
    3  * Like Template
     2/**
     3 * Like view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1533<header>
    1634<?php
    17 echo Kind_Taxonomy::get_before_kind( 'like' );
     35echo Kind_Taxonomy::get_before_kind( 'like' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1836if ( ! $embed ) {
    19         if ( ! empty( $url ) ) {
    20                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    21         } else {
    22                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
     37        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     38        if ( $author ) {
     39                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2340        }
    24         if ( $author ) {
    25                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    26         }
    27         if ( ! empty( $cite['publication'] ) ) {
    28                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     41        $site_name = Kind_View::get_site_name( $cite );
     42        if ( $site_name ) {
     43                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2944        }
    3045}
    … …  
    3449if ( $cite ) {
    3550        if ( $embed ) {
    36                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    37         } elseif ( array_key_exists( 'summary', $cite ) ) {
    38                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     51                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     52        } else {
     53                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3954        }
    4055}
    4156
    42 // Close Response
     57// Close Response.
    4358?>
    4459</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-listen.php

    r3001401 r3738687  
    11<?php
    2 /*
    3   Listen Template
     2/**
     3 * Listen view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1129$title     = Kind_View::get_cite_title( $cite );
    1230$duration  = $kind_post->get( 'duration', true );
    13 $rating  = $kind_post->get( 'rating', true );
     31$rating    = $kind_post->get( 'rating', true );
    1432if ( ! $duration ) {
    1533                $duration = calculate_duration( $kind_post->get( 'start' ), $kind_post->get( 'end' ) );
    … …  
    2240<header>
    2341<?php
    24 echo Kind_Taxonomy::get_before_kind( 'listen' );
     42echo Kind_Taxonomy::get_before_kind( 'listen' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    2543if ( ! $embed ) {
    2644        if ( $title ) {
    27                 echo $title;
     45                echo $title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
    2846        }
    2947        if ( $author ) {
    30                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     48                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    3149        }
    3250        if ( $site_name ) {
    33                 echo __( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>';
     51                echo esc_html__( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3452        }
    3553        if ( $duration ) {
    36                 echo '(' . Kind_View::display_duration( $duration ) . ')';
     54                echo '(' . Kind_View::display_duration( $duration ) . ')'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    3755        }
    3856}
    … …  
    4260if ( $cite ) {
    4361        if ( $embed ) {
    44                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    45         } elseif ( array_key_exists( 'summary', $cite ) ) {
    46                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     62                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     63        } else {
     64                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    4765        }
    4866}
    4967
    50 // Close Response
     68// Close Response.
    5169?>
    5270</section>
    5371
    54 <?php if ( $rating ) {
    55         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
    56 } ?>
     72<?php
     73if ( $rating ) {
     74        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
     75}
     76?>
    5777
    5878<?php
    5979if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    6080        $view = new Kind_Media_View( $photos, 'photo' );
    61         echo $view->get();
     81        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    6282}
    6383?>
  • indieweb-post-kinds/tags/4.0.0/views/kind-note.php

    r1828595 r3738687  
    11<?php
    2 /*
    3  * Note Template
     2/**
     3 * Note view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.4.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
  • indieweb-post-kinds/tags/4.0.0/views/kind-photo.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Photo Template
     2/**
     3 * Photo view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
     24
    625if ( $photos ) {
    726        $embed = null;
    … …  
    1130        if ( 1 === count( $photos ) ) {
    1231                $photos_attachment = new Kind_Post( $photos[0] );
    13                 $cite = $photos_attachment->get_cite();
    14                 $cite = $photos_attachment->normalize_cite( $cite );
    15                 $author = Kind_View::get_hcard( $cite['author'] );
     32                $cite              = $photos_attachment->get_cite();
     33                $cite              = $photos_attachment->normalize_cite( $cite );
     34                $author            = Kind_View::get_hcard( $cite['author'] );
    1635        }
    1736}
    … …  
    2140<header>
    2241<?php
    23 echo Kind_Taxonomy::get_before_kind( 'photo' );
     42echo Kind_Taxonomy::get_before_kind( 'photo' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    2443if ( ! empty( $cite['name'] ) ) {
    25         echo sprintf( '<span>%1s</span>', $cite['name'] );
     44        printf( '<span class="p-name">%1$s</span>', esc_html( $cite['name'] ) );
    2645}
    2746?>
    … …  
    2948<?php
    3049if ( $embed ) {
    31         printf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
     50        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
    3251} elseif ( $photos ) {
    3352        $view = new Kind_Media_View( $photos, 'photo' );
    34         echo $view->get();
    35 } ?>
     53        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
     54}
     55?>
    3656</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-read.php

    r3001401 r3738687  
    11<?php
    2 /*
    3  * Read Template
     2/**
     3 * Read view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    927}
    1028$read   = $kind_post->get( 'read-status', true );
    11 $rating  = $kind_post->get( 'rating', true );
     29$rating = $kind_post->get( 'rating', true );
    1230
    1331?>
    … …  
    1533<header>
    1634<?php
    17 echo Kind_Taxonomy::get_before_kind( 'read' );
     35echo Kind_Taxonomy::get_before_kind( 'read' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1836if ( ! $embed ) {
    1937        if ( $read ) {
    20                 echo sprintf( ' - <span class="p-read-status">%1s</span>', Kind_View::read_text( $read ) );
     38                printf( ' - <span class="p-read-status">%1$s</span>', esc_html( Kind_View::read_text( $read ) ) );
    2139        }
    22         if ( ! empty( $url ) ) {
    23                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    24         } else {
    25                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
     40        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     41        if ( $author ) {
     42                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2643        }
    27         if ( $author ) {
    28                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    29         }
    30         if ( empty( $cite['publication'] ) ) {
    31                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     44        $site_name = Kind_View::get_site_name( $cite );
     45        if ( $site_name ) {
     46                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3247        }
    3348}
    … …  
    3752if ( $cite ) {
    3853        if ( $embed ) {
    39                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    40         } elseif ( array_key_exists( 'summary', $cite ) && ! empty( $cite['summary'] ) ) {
    41                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     54                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     55        } else {
     56                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    4257        }
    4358}
    4459
    4560if ( $rating ) {
    46         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
     61        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
    4762}
    4863
    … …  
    5065if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    5166        $view = new Kind_Media_View( $photos, 'photo' );
    52         echo $view->get();
     67        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    5368}
    54 // Close Response
     69// Close Response.
    5570?>
    5671</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-rsvp.php

    r2394107 r3738687  
    11<?php
    2 /*
    3  * RSVP Template
     2/**
     3 * RSVP view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    826        return;
    927}
    10 $title  = isset( $cite['name'] ) ? $cite['name'] : $url;
    11 $rsvp   = $kind_post->get( 'rsvp', true );
     28$title = isset( $cite['name'] ) ? $cite['name'] : $url;
     29$rsvp  = $kind_post->get( 'rsvp', true );
    1230
    1331?>
    … …  
    1634<header>
    1735<?php
    18 echo Kind_Taxonomy::get_before_kind( 'rsvp' );
     36echo Kind_Taxonomy::get_before_kind( 'rsvp' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1937
    2038if ( ! $embed ) {
    2139        if ( $rsvp ) {
    22                 echo '<data class="p-rsvp" value="' . $rsvp . '">' . sprintf( Kind_View::rsvp_text( $rsvp ), $url, $title ) . '</data>';
     40                echo '<data class="p-rsvp" value="' . esc_attr( $rsvp ) . '">' . Kind_View::rsvp_text( $rsvp, $url, $title ) . '</data>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::rsvp_text() escapes the URL and name.
    2341        }
    2442}
    … …  
    2745<?php
    2846if ( $embed ) {
    29         echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    30 } elseif ( array_key_exists( 'summary', $cite ) ) {
    31         echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     47        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     48} else {
     49        echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3250}
    3351
    34 // Close Response
     52// Close Response.
    3553?>
    3654</section>
  • indieweb-post-kinds/tags/4.0.0/views/kind-video.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Video Template
     2/**
     3 * Video view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
    622
    7 $videos = $kind_post->get_video();
     23defined( 'ABSPATH' ) || exit;
     24
     25$videos   = $kind_post->get_video();
    826$duration = null;
    927if ( is_array( $videos ) ) {
    1028        if ( 1 === count( $videos ) && 0 !== $videos[0] ) {
    1129                $video_attachment = new Kind_Post( $videos[0] );
    12                 $cite = $video_attachment->get_cite();
    13                 $cite = $video_attachment->normalize_cite( $cite );
    14                 $author = Kind_View::get_hcard( $cite['author'] );
    15                
     30                $cite             = $video_attachment->get_cite();
     31                $cite             = $video_attachment->normalize_cite( $cite );
     32                $author           = Kind_View::get_hcard( $cite['author'] );
     33
    1634                if ( array_key_exists( 'duration', $cite ) ) {
    1735                        $duration = Kind_View::display_duration( $cite['duration'] );
    … …  
    2240}
    2341$first_photo = null;
    24 if ( is_countable( $photos ) ) {
     42if ( is_array( $photos ) && ! empty( $photos ) ) {
    2543        $first_photo = $photos[0];
    2644}
    2745if ( is_array( $cite ) && ! $videos ) {
    2846        if ( ! $embed ) {
    29                 $view = new Kind_Media_View( $url, 'video' );
     47                $view  = new Kind_Media_View( $url, 'video' );
    3048                $embed = $view->get();
    3149        }
    … …  
    3755<header>
    3856<?php
    39 echo Kind_Taxonomy::get_before_kind( 'video' );
     57echo Kind_Taxonomy::get_before_kind( 'video' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    4058if ( isset( $cite['name'] ) ) {
    41         echo sprintf( '<span>%1s</a>', $cite['name'] );
     59        printf( '<span class="p-name">%1$s</span>', esc_html( $cite['name'] ) );
    4260}
    4361
    4462if ( $author ) {
    45         echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     63        echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    4664}
    4765if ( $duration ) {
    48         printf( '(%1$s)', $duration );
     66        printf( '(%1$s)', $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    4967}
    5068
    … …  
    5472<?php
    5573if ( $embed ) {
    56         printf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
     74        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
    5775} elseif ( $videos ) {
    5876
    5977        $poster = wp_get_attachment_image_url( $first_photo, 'full' );
    60         $view = new Kind_Media_View( $videos, 'video' );
    61         echo $view->get();
     78        $view   = new Kind_Media_View( $videos, 'video' );
     79        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    6280}
    6381?>
  • indieweb-post-kinds/tags/4.0.0/views/kind-watch.php

    r3001401 r3738687  
    11<?php
    2 /*
    3   Watch Template
     2/**
     3 * Watch view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1129$title     = Kind_View::get_cite_title( $cite, $url );
    1230$duration  = $kind_post->get( 'duration', true );
    13 $rating  = $kind_post->get( 'rating', true );
     31$rating    = $kind_post->get( 'rating', true );
    1432if ( ! $duration ) {
    1533        $duration = calculate_duration( $kind_post->get( 'start' ), $kind_post->get( 'end' ) );
    … …  
    2139<header>
    2240<?php
    23 echo Kind_Taxonomy::get_before_kind( 'watch' );
     41echo Kind_Taxonomy::get_before_kind( 'watch' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    2442if ( ! $embed ) {
    2543        if ( $title ) {
    26                 echo $title;
     44                echo $title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
    2745        }
    2846        if ( $author ) {
    29                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     47                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    3048        }
    3149        if ( $site_name ) {
    32                 echo __( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>';
     50                echo esc_html__( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3351        }
    3452        if ( $duration ) {
    35                 echo Kind_View::display_duration( $duration );
     53                echo Kind_View::display_duration( $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    3654        }
    3755}
    … …  
    4159if ( $cite ) {
    4260        if ( $embed ) {
    43                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    44         } elseif ( array_key_exists( 'summary', $cite ) ) {
    45                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     61                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     62        } else {
     63                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    4664        }
    4765}
    4866
    49 // Close Response
     67// Close Response.
    5068?>
    5169</section>
    5270
    53 <?php if ( $rating ) {
    54         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
    55 } ?>
     71<?php
     72if ( $rating ) {
     73        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
     74}
     75?>
    5676
    5777
  • indieweb-post-kinds/tags/4.0.0/views/kind.php

    r3001401 r3738687  
    11<?php
    2 /*
    3   Default Template
    4  *      The Goal of this Template is to be a general all-purpose model that will be replaced by customization in other templates
     2/**
     3 * Default view, used for any kind without its own view.
     4 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.3.0
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725$site_name = Kind_View::get_site_name( $cite );
    … …  
    1129                $duration = calculate_duration( $kind_post->get( 'start' ), $kind_post->get( 'end' ) );
    1230}
    13 $rsvp = $kind_post->get( 'rsvp', true );
     31$rsvp   = $kind_post->get( 'rsvp', true );
    1432$rating = $kind_post->get( 'rating', true );
    1533
    … …  
    1836}
    1937
    20 // Add in the appropriate type
     38// Add in the appropriate type.
    2139if ( ! empty( $type ) ) {
    2240        $type = ( empty( $url ) ? 'p-' : 'u-' ) . $type;
    … …  
    2442?>
    2543
    26 <section class="h-cite response <?php echo $type; ?> ">
     44<section class="h-cite response <?php echo esc_attr( $type ); ?>">
    2745<header>
    2846<?php
    29 echo Kind_Taxonomy::get_before_kind( $kind );
     47echo Kind_Taxonomy::get_before_kind( $kind ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    3048if ( ! $embed ) {
    3149        if ( $title ) {
    32                 echo $title;
     50                echo $title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
    3351        }
    3452        if ( ! empty( $author ) ) {
    35                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     53                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    3654        }
    3755        if ( $site_name ) {
    38                 echo '<em> (' . $site_name . ')</em>';
     56                echo '<em> (' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3957        }
    4058        if ( in_array( $kind, array( 'jam', 'listen', 'play', 'read', 'watch', 'audio', 'video' ) ) ) {
    4159                if ( $duration ) {
    42                         echo ' ' . Kind_View::display_duration( $duration );
     60                        echo ' ' . Kind_View::display_duration( $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    4361                }
    4462        }
    … …  
    4967if ( $cite && is_array( $cite ) ) {
    5068        if ( $embed ) {
    51                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    52         } elseif ( array_key_exists( 'summary', $cite ) ) {
    53                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     69                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     70        } else {
     71                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    5472        }
    5573}
    5674
    57 // Close Response
     75// Close Response.
    5876?>
    5977</section>
    6078
    61 <?php if ( $rsvp && in_array( $kind, array( 'rsvp' ) ) ) {
    62         echo 'RSVP <span class="p-rsvp">' . $rsvp . '</span>';
     79<?php
     80if ( $rsvp && in_array( $kind, array( 'rsvp' ) ) ) {
     81        echo esc_html__( 'RSVP', 'indieweb-post-kinds' ) . ' <span class="p-rsvp">' . esc_html( $rsvp ) . '</span>';
    6382}
    6483
    6584if ( $rating ) {
    66         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
    67 } ?>
     85        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
     86}
     87?>
    6888
    6989<?php
  • indieweb-post-kinds/trunk/.gitattributes

    r2491232 r3738687  
    55/tests export-ignore
    66/bin export-ignore
    7 /node-modules export-ignore
     7/node_modules export-ignore
    88/sass export-ignore
    99
    … …  
    1717/readme.md export-ignore
    1818/phpcs.xml export-ignore
     19/phpcs-templates.xml export-ignore
    1920/phpunit.xml export-ignore
    20 /Gruntfile.js export-ignore
    21 /gruntlint.js export-ignore
     21/eslint.config.js export-ignore
  • indieweb-post-kinds/trunk/css/kind.admin.min.css

    r2572705 r3738687  
    1 .svg-icon svg{fill:currentColor;padding:2px;vertical-align:middle;top:-0.0625em}#kind-all svg{width:1rem;height:1rem;color:gray}#kind-menu svg,#kind-posts svg,#tempus-onthisday svg,#tempus-thisweek svg,.kind-titles svg{width:1.5rem;height:1.5rem;color:#fff;mix-blend-mode:difference}#kind-menu ul,#kind-posts ul,#tempus-onthisday ul,#tempus-thisweek ul,.kind-titles ul{list-style-type:none}.response svg{width:2rem;height:2rem;color:#fff;mix-blend-mode:difference}.response figure{margin:auto}#replybox-meta.is-loading .loading{visibility:visible}#replybox-meta label{display:block}#replybox-meta .field-row{overflow:auto;height:100%}#replybox-meta .half{float:left;width:46%;margin:0 1%}#replybox-meta .half:first-of-type{margin-left:0;width:48%}#replybox-meta .half:last-of-type{margin-right:0;width:49%}#replybox-meta .quarter{float:left;margin-left:2%;width:12%}#replybox-meta .eighth{float:left;margin-left:2%;margin-right:1%;width:6%}#replybox-meta input[type=number]{width:100%}#replybox-meta .three-quarters{float:left;width:85%}#replybox-meta .is-loading .loading{visibility:visible}#replybox-meta .loading{visibility:hidden;position:absolute;top:0;bottom:0;left:0;right:0;z-index:10;background-color:#000;background-color:rgba(0,0,0,.4)}#replybox-meta .loading-spinner{margin:0 auto;display:block;position:relative;top:2em}/*# sourceMappingURL=kind.admin.min.css.map */
     1:where(.svg-icon){display:inline-block;width:1em;height:1em;margin-right:.5rem;vertical-align:middle}:where(.svg-icon>span,.svg-icon svg){display:block;width:100%;height:100%}:where(.svg-icon svg){fill:currentColor}:where(#kind-all .svg-icon){width:1rem;height:1rem;color:gray}:where(#kind-menu,#kind-posts,#tempus-onthisday,#tempus-thisweek,.kind-titles) :where(.svg-icon){width:1.5rem;height:1.5rem;color:#fff;mix-blend-mode:difference}#kind-menu ul,#kind-posts ul,#tempus-onthisday ul,#tempus-thisweek ul,.kind-titles ul{list-style-type:none}:where(.response) :where(.svg-icon){width:2rem;height:2rem;color:#fff;mix-blend-mode:difference}.response figure{margin:auto}#replybox-meta.is-loading .loading{visibility:visible}#replybox-meta label{display:block}#replybox-meta .field-row{overflow:auto;height:100%}#replybox-meta .half{float:left;width:46%;margin:0 1%}#replybox-meta .half:first-of-type{margin-left:0;width:48%}#replybox-meta .half:last-of-type{margin-right:0;width:49%}#replybox-meta .quarter{float:left;margin-left:2%;width:12%}#replybox-meta .eighth{float:left;margin-left:2%;margin-right:1%;width:6%}#replybox-meta input[type=number]{width:100%}#replybox-meta .three-quarters{float:left;width:85%}#replybox-meta .is-loading .loading{visibility:visible}#replybox-meta .loading{visibility:hidden;position:absolute;top:0;bottom:0;left:0;right:0;z-index:10;background-color:#000;background-color:rgba(0,0,0,.4)}#replybox-meta .loading-spinner{margin:0 auto;display:block;position:relative;top:2em}/*# sourceMappingURL=kind.admin.min.css.map */
  • indieweb-post-kinds/trunk/css/kind.admin.min.css.map

    r2572705 r3738687  
    1 {"version":3,"sourceRoot":"","sources":["../sass/_kinds.scss","../sass/_meta-box.scss"],"names":[],"mappings":"AAAA,cACC,kBACA,YACA,sBACA,cAIA,cACC,WACA,YACA,WAKD,2FACC,aACA,cACA,WACA,0BAED,sFACC,qBAKM,cACQ,WACA,YACd,WACc,0BAEf,iBACC,YCnCF,mCACC,mBAIA,qBACC,cAED,0BACC,cACA,YAED,qBACC,WACA,UACA,YAED,mCACC,cACA,UAED,kCACC,eACA,UAED,wBACC,WACA,eACA,UAED,uBACC,WACA,eACA,gBACA,SAED,kCACC,WAED,+BACC,WACA,UAED,oCACC,mBAED,wBACC,kBACA,kBACA,MACA,SACA,OACA,QACA,WACA,sBACA,gCAED,gCACC,cACA,cACA,kBACA","file":"kind.admin.min.css"}
     1{"version":3,"sourceRoot":"..\/","sources":["sass\/_kinds.scss","sass\/_meta-box.scss"],"names":[],"mappings":"AAGA,kBACC,qBACA,UACA,WACA,mBACA,sBAGD,qCACC,cACA,WACA,YAGD,sBACC,kBAGD,4BACC,WACA,YACA,WAGD,iGACC,aACA,cACA,WACA,0BAIA,sFACC,qBAIF,oCACC,WACA,YACA,WACA,0BAIA,iBACC,YCjDF,mCACC,mBAIA,qBACC,cAED,0BACC,cACA,YAED,qBACC,WACA,UACA,YAED,mCACC,cACA,UAED,kCACC,eACA,UAED,wBACC,WACA,eACA,UAED,uBACC,WACA,eACA,gBACA,SAED,kCACC,WAED,+BACC,WACA,UAED,oCACC,mBAED,wBACC,kBACA,kBACA,MACA,SACA,OACA,QACA,WACA,sBACA,gCAED,gCACC,cACA,cACA,kBACA","file":"kind.admin.min.css"}
  • indieweb-post-kinds/trunk/css/kind.min.css

    r2588189 r3738687  
    1 .svg-icon svg{fill:currentColor;padding:2px;vertical-align:middle;top:-0.0625em}#kind-all svg{width:1rem;height:1rem;color:gray}#kind-menu svg,#kind-posts svg,#tempus-onthisday svg,#tempus-thisweek svg,.kind-titles svg{width:1.5rem;height:1.5rem;color:#fff;mix-blend-mode:difference}#kind-menu ul,#kind-posts ul,#tempus-onthisday ul,#tempus-thisweek ul,.kind-titles ul{list-style-type:none}.response svg{width:2rem;height:2rem;color:#fff;mix-blend-mode:difference}.response figure{margin:auto}.response{margin-bottom:10px;margin-top:10px;margin-left:20px;padding:5px;font-size:16px;border-bottom:inset #d6d6d6}.response .kind-icon{margin-right:3px}.response blockquote{margin-left:25px}.response a:link{text-decoration:none}.response a:hover{text-decoration:none}.response img{padding:0;margin:2px}ul.cites{list-style:none;display:inline}.kind-embeds{text-align:center;position:relative;width:100%;margin-top:5px}.kind-photo-thumbnail{margin-bottom:5px}/*# sourceMappingURL=kind.min.css.map */
     1:where(.svg-icon){display:inline-block;width:1em;height:1em;margin-right:.5rem;vertical-align:middle}:where(.svg-icon>span,.svg-icon svg){display:block;width:100%;height:100%}:where(.svg-icon svg){fill:currentColor}:where(#kind-all .svg-icon){width:1rem;height:1rem;color:gray}:where(#kind-menu,#kind-posts,#tempus-onthisday,#tempus-thisweek,.kind-titles) :where(.svg-icon){width:1.5rem;height:1.5rem;color:#fff;mix-blend-mode:difference}#kind-menu ul,#kind-posts ul,#tempus-onthisday ul,#tempus-thisweek ul,.kind-titles ul{list-style-type:none}:where(.response) :where(.svg-icon){width:2rem;height:2rem;color:#fff;mix-blend-mode:difference}.response figure{margin:auto}.response{margin-bottom:10px;margin-top:10px;margin-left:20px;padding:5px;font-size:16px;border-bottom:inset #d6d6d6}.response .kind-icon{margin-right:3px}.response blockquote{margin-left:25px}.response a:link{text-decoration:none}.response a:hover{text-decoration:none}.response img{padding:0;margin:2px}ul.cites{list-style:none;display:inline}.kind-embeds{text-align:center;position:relative;width:100%;margin-top:5px}.kind-photo-thumbnail{margin-bottom:5px}/*# sourceMappingURL=kind.min.css.map */
  • indieweb-post-kinds/trunk/css/kind.min.css.map

    r2588189 r3738687  
    1 {"version":3,"sourceRoot":"","sources":["../sass/_kinds.scss","../sass/_frontend.scss"],"names":[],"mappings":"AAAA,cACC,kBACA,YACA,sBACA,cAIA,cACC,WACA,YACA,WAKD,2FACC,aACA,cACA,WACA,0BAED,sFACC,qBAKM,cACQ,WACA,YACd,WACc,0BAEf,iBACC,YCnCF,UACI,mBACA,gBACA,iBACA,YACA,eACA,4BACF,qBACC,iBAED,qBACC,iBAGF,iBACC,qBAGD,kBACC,qBAGD,cACE,UACA,WAIH,SACI,gBACA,eAGJ,aACI,kBACA,kBACA,WACA,eAGJ,sBACI","file":"kind.min.css"}
     1{"version":3,"sourceRoot":"..\/","sources":["sass\/_kinds.scss","sass\/_frontend.scss"],"names":[],"mappings":"AAGA,kBACC,qBACA,UACA,WACA,mBACA,sBAGD,qCACC,cACA,WACA,YAGD,sBACC,kBAGD,4BACC,WACA,YACA,WAGD,iGACC,aACA,cACA,WACA,0BAIA,sFACC,qBAIF,oCACC,WACA,YACA,WACA,0BAIA,iBACC,YCjDF,UACI,mBACA,gBACA,iBACA,YACA,eACA,4BACF,qBACC,iBAED,qBACC,iBAGF,iBACC,qBAGD,kBACC,qBAGD,cACE,UACA,WAIH,SACI,gBACA,eAGJ,aACI,kBACA,kBACA,WACA,eAGJ,sBACI","file":"kind.min.css"}
  • indieweb-post-kinds/trunk/develop.txt

    r1787314 r3738687  
    1 == Functions ==
     1Developer notes for Post Kinds 4.0.0. Every function, method and hook listed here has a docblock in the code with its full parameters and return values.
    22
    3 `get_post_kind_slug($id)` - Return the kind slug for a given post. If `$id` is not specified, use current post.
    4 `get_post_kind($id)` - Return the kind string for a given post. If `$id` is not specified, use current post.
     3== Template functions ==
    54
    6 `has_post_kind($kind, $post)` - Returns true/false if kind is in post. If post is empty, then use current post
     5These global functions are loaded on `plugins_loaded`, from `includes/kind-functions.php`.
    76
    8 `get_kind_context_class( $class, $classtype )` - Returns the CSS class to be applied to the response/context if the kind is one for which there is context. Classtype defaults to u, other option is usually p. Sets the class to the kind slug and for specially specified slugs, sets appropriate mf2 classes as well. $class specifies any additional classes to be added.
     7`get_post_kind_slug( $post = null )` - Returns the kind slug of a post, or false if it has no kind. Defaults to the current post.
     8`get_post_kind( $post = null )` - Returns the translated kind name of a post, or false if it has no kind. Defaults to the current post.
     9`has_post_kind( $kinds = array(), $post = null )` - Returns whether a post has any of the given kinds, or any kind at all if `$kinds` is empty.
     10`set_post_kind( $post, $kind )` - Gives a post a kind. Returns a WP_Error if the post or the kind is not valid.
     11`get_post_kind_slugs()` - Returns the slugs of every registered kind.
     12`get_post_kind_string( $slug )` - Returns the translated name of a kind, or an empty string if it is not registered.
     13`get_post_kind_link( $kind )` - Returns the URL of a kind's archive.
     14`kind_display( $post_id = null )` - Prints the response markup for a post. Use it with the `kind_content_display` filter to place the markup in the theme.
     15`get_kind_view_part( $slug, $name = null, $args = null )` - Returns a rendered view. See Kind_View::get_view_part().
     16`kind_get_the_title( $post = null, $args = array() )` - Returns a post's title, or a generated one (the kind's icon or name and a cited name or excerpt) for a post without one.
     17`kind_get_the_link( $post = null, $cls = null, $date_cls = null )` - Returns a link to a post, using kind_get_the_title(), followed by its date.
     18`kind_flatten_array( $a )` - Removes empty values from an array and unwraps single-item lists, recursively.
     19`kind_src_url_in_content( $content )` - Returns every src attribute value in some markup, or 0 if there are none.
    920
     21== Registering kinds ==
    1022
     23`register_post_kind( $slug, $args )` - Registers a kind. Call it on `init`. `includes/register-kinds.php` registers the built-in kinds the same way, and Kind_Taxonomy::register_post_kind() documents every argument.
     24`set_post_kind_visibility( $slug, $show = true )` - Shows or hides a kind in Settings, including the kinds reserved for future use.
    1125
    12 ==  Filters ==
     26== Time functions ==
    1327
    14 `get_the_kind` - Filter get_the_kinds
     28Also global, from `includes/time-functions.php`.
    1529
    16 `the_kinds` - Filter get_the_kinds_list
     30`kind_safe_datetime( $value, $timezone = null, $immutable = true )` - Since 4.0.0. Creates a date from a value, returning false instead of throwing on invalid input.
     31`kind_safe_interval( $value )` - Since 4.0.0. Creates a DateInterval from an ISO 8601 duration such as `PT3M30S`, returning false instead of throwing on invalid input.
     32`calculate_duration( $start_string, $end_string )` - Returns the DateInterval between two dates, or false.
     33`date_interval_to_iso8601( $interval )` - Converts a DateInterval to an ISO 8601 duration string.
     34`build_iso8601_time( $date, $time, $offset )` - Joins a date, time and offset into an ISO 8601 string.
     35`build_iso8601_duration( $values )` - Builds an ISO 8601 duration from its pieces (Y, M, D, H, I, S).
     36`get_gmt_offsets()` - Returns the UTC offsets in use by any time zone, sorted.
     37`get_default_offset()` - Returns the site's UTC offset, such as `-05:00`.
     38`tz_seconds_to_offset( $seconds )`, `tz_offset_to_seconds( $offset )` - Convert between a UTC offset in seconds and an offset string.
     39`tz_seconds_to_timezone( $seconds )`, `tz_timezone_to_seconds( $timezone )` - Convert between a UTC offset in seconds and a DateTimeZone.
    1740
    18 `kind_classes` - Filter get_kind_class
     41== Classes ==
    1942
    20 `kind_verb` - Filter get_kind_verbs
     43`Kind_Post` reads and writes a post's response data. Create one with a post ID or WP_Post. Its main methods:
    2144
    22 `kind-response-display` - Filters the output being added to the_content or to custom location in theme
     45* `get_kind()`, `get_cite( $key = null )`, `get_author()`, `get_photo()`, `get_audio()`, `get_video()`, `get_duration()`, `get_datetime_property( $property )` and `get( $key )` read data.
     46* `set( $key, $value )`, `set_duration( $value )`, `set_datetime_property( $key, $value )` and `delete( $key )` write it. Response data is stored as post meta prefixed `mf2_`.
     47* `get_cite_type()` - Since 4.0.0. Returns the jf2 type of the post's citation: `card` for a checkin, `food` for eat and drink, and `cite` for every other kind.
     48* `Kind_Post::get_attachment_id( $url )` - Since 4.0.0. Static. A cached wrapper for attachment_url_to_postid().
     49
     50`Kind_Time` - Since 4.0.0. Static helpers for the editor's date, time and duration fields: `display_formatted_datetime()`, `divide_datetime()`, `build_datetime()`, `get_datetime_offset()`, `divide_interval()` and `build_interval()`.
     51
     52`Kind_View` builds the response markup. `Kind_View::get_display( $post_id = null )` returns it for a post, `Kind_View::get_view_part( $slug, $name, $args = null )` renders one view, and `Kind_View::get_hcard( $author, $args = null )` returns an author's h-card. Since 4.0.0, its helpers return escaped markup; the 4.0.0 changelog lists the ones that changed.
     53
     54`Kind_Media_Metadata::has_media( $post )` - Since 4.0.0. Returns whether a post may show audio or video. The media fragment script loads only for these posts.
     55
     56`Kind_Upgrade` - Since 4.0.0. Upgrades stored post data in the background, in batches, after the plugin is updated. It gives published posts without a kind one, moves citations out of older storage locations, repairs citations that earlier versions damaged, and records the media in each post.
     57
     58== WP-CLI ==
     59
     60`wp post-kinds upgrade` - Since 4.0.0. Runs the upgrade now instead of in the background. It is safe to run more than once.
     61
     62* `--dry-run` - Report what would change without saving.
     63* `--post=<id>` - Upgrade only this post.
     64
     65== REST API ==
     66
     67* The `kind` field on posts reads and sets a post's kind slug.
     68* `GET /wp-json/post-kinds/1.0/fields?kind={slug}` returns a kind's registration, including its field schema. An unknown kind returns a 404. It requires the `edit_posts` capability.
     69* The editor fetches details for a cited URL from the bundled Parse This library's `parse-this/1.0/parse` route.
     70
     71== Filters ==
     72
     73Display:
     74
     75`kind_content_display` - Whether to add response markup to post content, excerpts and feeds automatically. Return false to place it with kind_display() instead. Default true.
     76`kind_response_display` - The response markup for a post. Receives the markup and the post ID.
     77`kind_view_paths` - The folders searched for views before the plugin's `views` folder. Default the theme's `kind_views` folder.
     78`kind_icon_display` - Whether to show the kind's icon or name before the response. Return false to hide it and remove the option from Settings.
     79`post_kinds_icon_attributes` - The HTML attributes on the span that wraps a kind icon. Output as-is, so it must be escaped. Default empty since 4.0.0; before that, an inline style.
     80`kind_get_the_title_content` - The text of a generated title. Receives escaped text and the post.
     81`kind_get_the_title_before` - The markup before a generated title, the kind's icon or name by default. Output as-is, so it must be escaped.
     82`get_hcard_data` - The author data before it becomes an h-card.
     83`get_hcard` - The h-card markup for an author. Return non-empty, escaped markup to replace the plugin's own.
     84`post_kind_embed_approvelist` - The domains whose cited URLs are embedded with oEmbed.
     85`kind_post_type_string` - The phrase for what a known site's URL points to, such as "a video" for YouTube.
     86
     87Kinds and archives:
     88
     89`kind_include` - The kinds offered in the editor and listed by the Kind Menu widget. Default the kinds selected in Settings, plus note, reply and article.
     90`kind_base_slug` - Since 4.0.0. The start of kind archive URLs, as in `/kind/like/`. Default the Kind base on Settings > Permalinks, or `kind`.
     91`kind_rewrite_taxonomies` - The taxonomies that get `/kind/{kind}/{taxonomy}/{term}/` archives. Default post_tag, category and series.
     92`kind_exclude_slug` - The slug for archives that exclude kinds, as in `/exclude/kind/note,like/`. Default `exclude`.
     93`kind_photos_slug` - The slug for the archive of posts with photos. Default `photos`.
     94`kind_firehose_slug` - The slug for the feed that includes every kind. Default `firehose`.
     95
     96After changing a slug or the rewrite taxonomies, flush rewrite rules: visit Settings > Permalinks and save.
     97
     98Upgrade:
     99
     100`kind_upgrade_batch_size` - Since 4.0.0. The number of posts the background upgrade processes per batch. Default 50.
     101
     102Micropub (these run only when the Micropub plugin is active):
     103
     104`post_kinds_micropub_enrich` - Since 4.0.0. Whether to fill in citation URLs from Micropub requests with data parsed from the cited page. Return false to store citations exactly as the client sent them. Default true.
     105`post_kinds_micropub_parse_timeout` - Since 4.0.0. The timeout, in seconds, for parsing a citation while the client waits. Citations that time out are retried in the background. Default 5.
     106`post_kinds_micropub_unavailable_retry_delay` - Since 4.0.0. How long to wait before retrying, once, a citation whose page returned 404, 401 or 403. Default 6 hours.
     107`post_kinds_micropub_post_types` - Since 4.0.0. The post types advertised to Micropub clients in `q=config`.
     108
     109== Actions ==
     110
     111`change_kind` - Fires when a post's kind changes. Receives the post ID and the old and new kind slugs; either slug can be an empty string.
     112`post_kinds_citation_enrichment` - Since 4.0.0. Fires after each attempt to parse a Micropub citation. Receives the log entry and the post ID.
     113
     114== Deprecated ==
     115
     116These still work, but report a deprecation notice when WP_DEBUG is on.
     117
     118* Since 4.0.0, the global functions `display_formatted_datetime()`, `divide_datetime()`, `build_datetime()`, `get_datetime_offset()`, `divide_interval()` and `build_interval()`. Use the Kind_Time methods of the same names. They will be removed in 5.0.0.
     119* Since 4.0.0, the `MF2_Post` class. Use Kind_Post. It will be removed in a future major version.
     120
     121== Styling icons ==
     122
     123Kind icons are `<span class="svg-icon svg-{kind}">` elements wrapping an SVG. The plugin's stylesheet sizes the span, and the SVG fills it. Its rules use `:where()`, so they have no specificity and any theme rule wins. For example, `.svg-icon { width: 1rem; height: 1rem; }` resizes every icon, and `#kind-menu .svg-icon` only those in the Kind Menu widget. Icons take their colour from the `color` property.
     124
     125== Overriding views ==
     126
     127The markup added to each post comes from a view file in the plugin's `views` folder: `kind-{slug}.php` for a kind (for example `kind-like.php`), or `kind.php` for a kind without its own view.
     128
     129To change a view, copy it into a `kind_views` folder in your theme and edit the copy. If a child theme has a `kind_views` folder, it is used instead of the parent theme's; the two aren't combined. For each post, the plugin looks for `kind-{slug}.php` in your theme and then in the plugin, and only then for `kind.php` in the same order. So a theme `kind.php` replaces only the kinds that have no view of their own. The `kind_view_paths` filter changes which folders are searched before the plugin's.
     130
     131Copies don't receive updates. After upgrading, compare them with the plugin's views. Since 4.0.0, views escape everything they print, and the `Kind_View` helpers return escaped markup.
     132
     133Each view is rendered by `Kind_View::get_view_part()` with these variables in scope:
     134
     135* `$kind_post` (Kind_Post) - the post being displayed.
     136* `$kind` (string) - the kind's slug, such as `like`.
     137* `$type` (string) - the kind's microformats property, such as `like-of`.
     138* `$cite` (array) - the citation, normalized by `Kind_Post::normalize_cite()`, so every key is present. `name` falls back to the URL.
     139* `$url` (string) - the cited URL.
     140* `$author` (string|false) - the cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none. `$cite['authors']` lists each author as a card with `name`, `url` and `photo`.
     141* `$embed` (string) - embed markup for the cited URL, or an empty string.
     142* `$photos` (int[]|false) - attachment IDs of the post's photos, from `Kind_Post::get_photo()`.
     143* `$args` (array) - the arguments passed to `Kind_View::get_view_part()`; `post_id` is the post's ID.
  • indieweb-post-kinds/trunk/includes/class-kind-config.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Post Kind Configuration Class
     3 * Kind_Config class.
    44 *
    5  * @package Post Kinds
    6  * Sets Up Configuration Options for the Plugin.
     5 * @package Post_Kinds
     6 * @since   2.0.2
    77 */
    88
     9defined( 'ABSPATH' ) || exit;
     10
    911/**
    10  * Static Class to Configure Admin Options.
     12 * Registers the plugin settings and builds the settings page.
    1113 *
    12  * @package Post Kinds
     14 * @since 2.0.2
    1315 */
    1416class Kind_Config {
    … …  
    1618        /**
    1719         * Function to Initialize the Configuration.
     20         *
     21         * @since 2.0.2
    1822         *
    1923         * @access public
    … …  
    2226                add_action( 'admin_init', array( static::class, 'admin_init' ) );
    2327                add_action( 'admin_menu', array( static::class, 'admin_menu' ), 11 );
    24                 // Add post help tab
     28                // Add post help tab.
    2529                add_action( 'load-post.php', array( static::class, 'add_post_help_tab' ), 20 );
    26 
    27                 $args = array(
    28                         'type'         => 'array',
    29                         'description'  => 'Kinds Enabled on This Site',
    30                         'show_in_rest' => false,
    31                         'default'      => array( 'article', 'reply', 'bookmark' ),
     30                add_action( 'load-options-permalink.php', array( static::class, 'save_kind_base' ) );
     31
     32                $args = array(
     33                        'type'              => 'array',
     34                        'description'       => 'Kinds Enabled on This Site',
     35                        'show_in_rest'      => false,
     36                        'default'           => array( 'article', 'reply', 'bookmark' ),
     37                        'sanitize_callback' => array( static::class, 'sanitize_kind_list' ),
    3238                );
    3339                register_setting( 'iwt_options', 'kind_termslist', $args );
    3440                $args = array(
    35                         'type'         => 'string',
    36                         'description'  => 'Default Kind',
    37                         'show_in_rest' => false,
    38                         'default'      => 'note',
     41                        'type'              => 'string',
     42                        'description'       => 'Default Kind',
     43                        'show_in_rest'      => false,
     44                        'default'           => 'note',
     45                        'sanitize_callback' => array( static::class, 'sanitize_default_kind' ),
    3946                );
    4047                register_setting( 'iwt_options', 'kind_default', $args );
    4148                $args = array(
    42                         'type'         => 'boolean',
    43                         'description'  => 'Rich Embed Support for Whitelisted Sites',
    44                         'show_in_rest' => false,
    45                         'default'      => 1,
     49                        'type'              => 'boolean',
     50                        'description'       => 'Rich Embed Support for Whitelisted Sites',
     51                        'show_in_rest'      => false,
     52                        'default'           => 1,
     53                        'sanitize_callback' => array( static::class, 'sanitize_checkbox' ),
    4654                );
    4755                register_setting( 'iwt_options', 'kind_embeds', $args );
    4856                $args = array(
    49                         'type'         => 'array',
    50                         'description'  => 'Kinds Showing in Main Archive',
    51                         'show_in_rest' => false,
    52                         'default'      => array(),
     57                        'type'              => 'array',
     58                        'description'       => 'Kinds Showing in Main Archive',
     59                        'show_in_rest'      => false,
     60                        'default'           => array(),
     61                        'sanitize_callback' => array( static::class, 'sanitize_kind_list' ),
    5362                );
    5463                register_setting( 'iwt_options', 'kind_firehose', $args );
    5564
    5665                $args = array(
    57                         'type'         => 'boolean',
    58                         'description'  => 'Response Information Should Be After Content',
    59                         'show_in_rest' => false,
    60                         'default'      => 0,
     66                        'type'              => 'boolean',
     67                        'description'       => 'Response Information Should Be After Content',
     68                        'show_in_rest'      => false,
     69                        'default'           => 0,
     70                        'sanitize_callback' => array( static::class, 'sanitize_checkbox' ),
    6171                );
    6272                register_setting( 'iwt_options', 'kind_bottom', $args );
    6373                $args = array(
    64                         'type'         => 'string',
    65                         'description'  => 'Display Preferences for Before Kind',
    66                         'show_in_rest' => false,
    67                         'default'      => 'icon',
     74                        'type'              => 'string',
     75                        'description'       => 'Display Preferences for Before Kind',
     76                        'show_in_rest'      => false,
     77                        'default'           => 'icon',
     78                        'sanitize_callback' => array( static::class, 'sanitize_display' ),
    6879                );
    6980                register_setting( 'iwt_options', 'kind_display', $args );
    7081                $args = array(
    71                         'type'         => 'string',
    72                         'description'  => 'KSES Content Protection on Responses',
    73                         'show_in_rest' => false,
    74                         'default'      => str_replace( '},"', "},\r\n\"", wp_json_encode( wp_kses_allowed_html( 'post' ), 128 ) ),
     82                        'type'              => 'string',
     83                        'description'       => 'KSES Content Protection on Responses',
     84                        'show_in_rest'      => false,
     85                        'default'           => str_replace( '},"', "},\r\n\"", wp_json_encode( wp_kses_allowed_html( 'post' ), 128 ) ),
     86                        'sanitize_callback' => array( static::class, 'sanitize_kses' ),
    7587                );
    7688                register_setting( 'iwt_options', 'kind_kses', $args );
    7789                $args = array(
    78                         'type'         => 'boolean',
    79                         'description'  => 'Automatically add the Kind to the Title',
    80                         'show_in_rest' => false,
    81                         'default'      => 0,
     90                        'type'              => 'boolean',
     91                        'description'       => 'Automatically add the Kind to the Title',
     92                        'show_in_rest'      => false,
     93                        'default'           => 0,
     94                        'sanitize_callback' => array( static::class, 'sanitize_checkbox' ),
    8295                );
    8396                register_setting( 'iwt_options', 'kind_title', $args );
     97                $args = array(
     98                        'type'              => 'string',
     99                        'description'       => 'Kind Base',
     100                        'show_in_rest'      => false,
     101                        'default'           => '',
     102                        'sanitize_callback' => array( static::class, 'sanitize_kind_base' ),
     103                );
     104                register_setting( 'permalink', 'kind_base', $args );
     105        }
     106
     107        /**
     108         * Sanitizes the kind_base setting.
     109         *
     110         * @since 4.0.0
     111         *
     112         * @param mixed $value Submitted value.
     113         * @return string The base, such as 'type' or 'blog/type', or an empty string for the default.
     114         */
     115        public static function sanitize_kind_base( $value ) {
     116                if ( ! is_string( $value ) ) {
     117                        return '';
     118                }
     119                $segments = array_filter( array_map( 'sanitize_title', explode( '/', $value ) ) );
     120                $base     = implode( '/', $segments );
     121                return 'kind' === $base ? '' : $base;
     122        }
     123
     124        /**
     125         * Saves the Kind base field on Settings > Permalinks.
     126         *
     127         * Core's permalink page doesn't save fields that plugins add, so this runs
     128         * before it, with its nonce. Core flushes the rewrite rules when the page
     129         * reloads.
     130         *
     131         * @since 4.0.0
     132         */
     133        public static function save_kind_base() {
     134                if ( ! isset( $_POST['kind_base'] ) || ! is_string( $_POST['kind_base'] ) || ! current_user_can( 'manage_options' ) ) {
     135                        return;
     136                }
     137                check_admin_referer( 'update-permalink' );
     138                // Sanitized by sanitize_kind_base(), the setting's sanitize callback.
     139                update_option( 'kind_base', wp_unslash( $_POST['kind_base'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
     140        }
     141
     142        /**
     143         * Outputs the Kind base field on Settings > Permalinks.
     144         *
     145         * @since 4.0.0
     146         */
     147        public static function kind_base_callback() {
     148                printf(
     149                        '<input name="kind_base" id="kind_base" type="text" value="%1$s" class="regular-text code" placeholder="kind" />',
     150                        esc_attr( get_option( 'kind_base', '' ) )
     151                );
     152        }
     153
     154        /**
     155         * Sanitizes a list of kinds: the kind_termslist and kind_firehose settings.
     156         *
     157         * @since 4.0.0
     158         *
     159         * @param mixed $value Submitted value.
     160         * @return string[] Registered kind slugs.
     161         */
     162        public static function sanitize_kind_list( $value ) {
     163                if ( ! is_array( $value ) ) {
     164                        return array();
     165                }
     166                $value = array_map( 'sanitize_key', array_filter( $value, 'is_string' ) );
     167                return array_values( array_unique( array_intersect( $value, Kind_Taxonomy::get_kind_list() ) ) );
     168        }
     169
     170        /**
     171         * Sanitizes the default kind setting.
     172         *
     173         * @since 4.0.0
     174         *
     175         * @param mixed $value Submitted value.
     176         * @return string A registered kind slug, or note.
     177         */
     178        public static function sanitize_default_kind( $value ) {
     179                $value = is_string( $value ) ? sanitize_key( $value ) : '';
     180                return in_array( $value, Kind_Taxonomy::get_kind_list(), true ) ? $value : 'note';
     181        }
     182
     183        /**
     184         * Sanitizes the display setting for the text before a kind.
     185         *
     186         * @since 4.0.0
     187         *
     188         * @param mixed $value Submitted value.
     189         * @return string icon, text, both or hide. Defaults to icon.
     190         */
     191        public static function sanitize_display( $value ) {
     192                return in_array( $value, array( 'icon', 'text', 'both', 'hide' ), true ) ? $value : 'icon';
     193        }
     194
     195        /**
     196         * Sanitizes a checkbox setting.
     197         *
     198         * @since 4.0.0
     199         *
     200         * @param mixed $value Submitted value.
     201         * @return int 1 if checked, otherwise 0.
     202         */
     203        public static function sanitize_checkbox( $value ) {
     204                return empty( $value ) ? 0 : 1;
     205        }
     206
     207        /**
     208         * Sanitizes the allowed HTML setting, a JSON object as wp_kses() expects.
     209         *
     210         * Invalid JSON is rejected and the previous value kept.
     211         *
     212         * @since 4.0.0
     213         *
     214         * @param mixed $value Submitted value.
     215         * @return string JSON-encoded allowed HTML.
     216         */
     217        public static function sanitize_kses( $value ) {
     218                $decoded = is_string( $value ) ? json_decode( $value, true ) : null;
     219                if ( is_array( $decoded ) ) {
     220                        return $value;
     221                }
     222                // The settings error API is only loaded in the admin.
     223                if ( function_exists( 'add_settings_error' ) ) {
     224                        add_settings_error( 'kind_kses', 'kind_kses_invalid', __( 'The allowed HTML must be a valid JSON object. The previous value has been kept.', 'indieweb-post-kinds' ) );
     225                }
     226                $previous = get_option( 'kind_kses' );
     227                return is_string( $previous ) ? $previous : '';
     228        }
     229
     230        /**
     231         * Returns the kinds enabled in settings (the kind_termslist option).
     232         *
     233         * Always returns an array of registered kind slugs, whatever is stored: the
     234         * option can be missing, false or hold kinds that are no longer registered.
     235         * Note, which is always available, is not included unless it was saved.
     236         *
     237         * @since 4.0.0
     238         *
     239         * @return string[] Kind slugs.
     240         */
     241        public static function get_termslist() {
     242                $termslist = get_option( 'kind_termslist' );
     243                if ( ! is_array( $termslist ) ) {
     244                        return array();
     245                }
     246                return array_values( array_intersect( $termslist, Kind_Taxonomy::get_kind_list() ) );
    84247        }
    85248
    86249        /**
    87250         * Function to Set up Settings.
     251         *
     252         * @since 2.0.2
    88253         *
    89254         * @access public
    … …  
    92257                add_action( 'admin_bar_menu', array( static::class, 'dashbar_links' ), 20 );
    93258                add_action( 'admin_bar_menu', array( static::class, 'remove_dashbar_post' ), 200 );
     259                add_settings_field(
     260                        'kind_base',
     261                        __( 'Kind base', 'indieweb-post-kinds' ),
     262                        array( static::class, 'kind_base_callback' ),
     263                        'permalink',
     264                        'optional',
     265                        array( 'label_for' => 'kind_base' )
     266                );
    94267                add_settings_section(
    95268                        'iwt-content',
    … …  
    189362                        )
    190363                );
    191                 // Add Query Var to Admin
     364                // Add Query Var to Admin.
    192365                add_filter( 'query_vars', array( static::class, 'query_var' ) );
    193366        }
    … …  
    195368        /**
    196369         * Function to add our kindurl query var.
     370         *
     371         * @since 2.3.3
    197372         *
    198373         * @access public
    … …  
    209384         * Function to remove "post" from the "New" admin bar section.
    210385         *
    211          * @access public
    212          *
    213          * @param WP_Admin_Bar $wp_admin_bar
     386         * @since 3.0.7
     387         *
     388         * @access public
     389         *
     390         * @param WP_Admin_Bar $wp_admin_bar The admin bar.
    214391         */
    215392        public static function remove_dashbar_post( $wp_admin_bar ) {
    … …  
    220397         * Function to add our Post Kind post links to the "New" admin bar section.
    221398         *
    222          * @access public
    223          * @param WP_Admin_Bar $wp_admin_bar
     399         * @since 3.0.7
     400         *
     401         * @access public
     402         *
     403         * @param WP_Admin_Bar $wp_admin_bar The admin bar.
    224404         */
    225405        public static function dashbar_links( $wp_admin_bar ) {
    226                 $termslist = get_option( 'kind_termslist' );
    227                 // Note can never be removed
     406                $termslist = self::get_termslist();
     407                // Note can never be removed.
    228408                array_unshift( $termslist, 'note' );
    229                 foreach ( $termslist as $term ) {
     409                foreach ( array_unique( $termslist ) as $term ) {
    230410                        $wp_admin_bar->add_menu(
    231411                                array(
    … …  
    242422         * Adds Options Page for Plugin Options.
    243423         *
     424         * @since 2.0.2
     425         *
    244426         * @access public
    245427         */
    … …  
    249431                        add_submenu_page(
    250432                                'indieweb',
    251                                 __( 'Post Kinds', 'indieweb-post-kinds' ), // page title
    252                                 __( 'Post Kinds', 'indieweb-post-kinds' ), // menu title
    253                                 'manage_options', // access capability
     433                                __( 'Post Kinds', 'indieweb-post-kinds' ), // page title.
     434                                __( 'Post Kinds', 'indieweb-post-kinds' ), // menu title.
     435                                'manage_options', // access capability.
    254436                                'kind_options',
    255437                                array( static::class, 'options_form' )
    … …  
    263445         * Callback for Options on Options Page.
    264446         *
     447         * @since 2.0.2
     448         *
    265449         * @access public
    266450         */
    … …  
    270454        /**
    271455         * Generate a Checkbox.
     456         *
     457         * @since 2.0.2
    272458         *
    273459         * @access public
    … …  
    280466                $option  = get_option( $args['name'] );
    281467                $checked = intval( $option ?? 0 );
    282                 printf( '<input name="%1$s" type="hidden" value="0" />', esc_attr( $args['name'] ) ); // phpcs:ignore
    283                 printf( '<input name="%1$s" type="checkbox" value="1" %2$s />', esc_attr( $args['name'] ), checked( 1, $checked, false) ); // phpcs:ignore
     468                printf( '<input name="%1$s" type="hidden" value="0" />', esc_attr( $args['name'] ) );
     469                printf( '<input name="%1$s" type="checkbox" value="1" %2$s />', esc_attr( $args['name'] ), checked( 1, $checked, false ) );
    284470        }
    285471
    286472        /**
    287473         * Generate a Textbox.
     474         *
     475         * @since 2.0.2
    288476         *
    289477         * @access public
    … …  
    296484                $option = get_option( $args['name'] );
    297485                if ( is_array( $option ) ) {
    298                         $option = print_r( $option, true );
    299                 }
    300                 echo "<textarea rows='10' cols='50' class='large-text code' name='" . esc_attr( $args['name'] ) . "'>" . $option . '</textarea> '; // phpcs:ignore
     486                        $option = wp_json_encode( $option, JSON_PRETTY_PRINT );
     487                }
     488                echo "<textarea rows='10' cols='50' class='large-text code' name='" . esc_attr( $args['name'] ) . "'>" . esc_textarea( (string) $option ) . '</textarea> ';
    301489        }
    302490
    303491        /**
    304492         * Generate a Term List.
     493         *
     494         * @since 2.5.0
    305495         *
    306496         * @access public
    … …  
    309499                $terms = Kind_Taxonomy::get_kind_list();
    310500                // Hide these terms until ready for use for now.
    311                 $termslist = get_option( 'kind_termslist' );
     501                $termslist = self::get_termslist();
    312502                echo '<div id="kind-all">';
    313503                foreach ( $terms as $term ) {
    314504                        $value = Kind_Taxonomy::get_post_kind_info( $term );
    315505                        if ( $value->show ) {
    316                                 printf( '<input name="kind_termslist[]" type="checkbox" value="%1$s" %2$s />', esc_attr( $term ), checked( in_array( $term, $termslist, true ), true, false ) ); // phpcs:ignore
    317                                 printf( '%1$s<strong>%2$s</strong> - %3$s<br />', Kind_Taxonomy::get_icon( $term ), sanitize_text_field( $value->singular_name ), sanitize_text_field( $value->description ) );  // phpcs:ignore
     506                                printf( '<input name="kind_termslist[]" type="checkbox" value="%1$s" %2$s />', esc_attr( $term ), checked( in_array( $term, $termslist, true ), true, false ) );
     507                                printf( '%1$s<strong>%2$s</strong> - %3$s<br />', Kind_Taxonomy::get_icon( $term ), esc_html( $value->singular_name ), esc_html( $value->description ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon markup.
    318508                        }
    319509                }
    … …  
    322512
    323513        /**
    324          * Generate a Term List.
    325          *
    326          * @access public
     514         * Outputs a multiple select of kinds for a setting.
     515         *
     516         * @since 3.6.0
     517         *
     518         * @access public
     519         *
     520         * @param array $args Field arguments. 'name' is the option name.
    327521         */
    328522        public static function kindmultiselect_callback( array $args ) {
    329                 $terms   = get_option( 'kind_termslist' );
     523                $terms   = self::get_termslist();
    330524                $terms[] = 'note';
     525                $terms   = array_unique( $terms );
    331526
    332527                $kindlist = get_option( $args['name'] );
    … …  
    339534                foreach ( $terms as $term ) {
    340535                        $value = Kind_Taxonomy::get_post_kind_info( $term );
    341                         printf( '<option value="%1$s" %2$s />%3$s</option>', esc_attr($term), selected( in_array( $term, $kindlist ), true, false ), sanitize_text_field( $value->singular_name ) );  // phpcs:ignore
     536                        printf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $term ), selected( in_array( $term, $kindlist, true ), true, false ), esc_html( $value->singular_name ) );
    342537                }
    343538                echo '</select>';
    … …  
    349544         * Generate a Kind Select List.
    350545         *
     546         * @since 2.3.3
     547         *
    351548         * @access public
    352549         */
    353550        public static function defaultkind_callback() {
    354                 $terms   = get_option( 'kind_termslist' );
     551                $terms   = self::get_termslist();
    355552                $terms[] = 'note';
     553                $terms   = array_unique( $terms );
    356554                sort( $terms, SORT_STRING );
    357555
    … …  
    361559                foreach ( $terms as $term ) {
    362560                        $value = Kind_Taxonomy::get_post_kind_info( $term );
    363                         printf( '<option value="%1$s" %2$s />%3$s</option>', esc_attr($term), selected( $term, $defaultkind, false ), sanitize_text_field( $value->singular_name ) );  // phpcs:ignore
     561                        printf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $term ), selected( $term, $defaultkind, false ), esc_html( $value->singular_name ) );
    364562                }
    365563                echo '</select>';
    … …  
    368566        /**
    369567         * Generate Radio Options.
     568         *
     569         * @since 2.6.6
    370570         *
    371571         * @access public
    … …  
    376576                $display = get_option( 'kind_display' );
    377577                foreach ( $args['options'] as $key => $value ) {
    378                         printf( '<input id="%1$s" name="%1$s" type="radio" value="%2$s" class="%3$s" %4$s />%5$s<br />', esc_attr( $args['name'] ), esc_attr( $key ), esc_attr( $args['class'] ), checked( $key, $display, false ), sanitize_text_field( $value ) ); // phpcs:ignore
     578                        printf( '<input id="%1$s" name="%1$s" type="radio" value="%2$s" class="%3$s" %4$s />%5$s<br />', esc_attr( $args['name'] ), esc_attr( $key ), esc_attr( $args['class'] ), checked( $key, $display, false ), esc_html( $value ) );
    379579                }
    380580        }
    … …  
    382582        /**
    383583         * Generate Options Form.
     584         *
     585         * @since 2.0.2
    384586         *
    385587         * @access public
    … …  
    406608        /**
    407609         * Function to generate a help tab in the WordPress admin help tab.
     610         *
     611         * @since 2.3.0
    408612         *
    409613         * @access public
  • indieweb-post-kinds/trunk/includes/class-kind-fields.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Kind Fields Class
     3 * Kind_Fields class.
    44 *
    5  * @package Post Kind
    6  * Used to Generate Form Fields for a Post UI
     5 * @package Post_Kinds
     6 * @since   3.3.4
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Generates form fields for the posting UI from a kind's property schema.
    713 *
    8  * Fields currently supported and their options{
    9         * datetime
    10         * duration
    11         * author - is a representation of an author and consists of name, url, and photo by default
    12         * select
    13                 * options - associative array of values for the select. Key being the value and the value being the description
    14         * url
    15         * text
    16         * list
    17         * textarea
    18         * number
    19         * cite - is a representation of an h-cite object
    20 * }
     14 * Supported field types and their options:
     15 * - datetime
     16 * - duration
     17 * - author: a name, URL and photo by default.
     18 * - select: `options` is an associative array of value => label.
     19 * - url
     20 * - text
     21 * - list
     22 * - textarea
     23 * - number
     24 * - cite: an h-cite object.
     25 *
     26 * @since 3.3.4
    2127 */
    22 
    2328class Kind_Fields {
    2429
     30        /**
     31         * Returns a list of time zones with translated labels.
     32         *
     33         * @since 3.3.4
     34         *
     35         * @return string[] Labels, keyed by time zone identifier.
     36         */
    2537        public static function timezone_list() {
    2638                return array(
    … …  
    279291        }
    280292
     293        /**
     294         * Returns the UTC offsets in use by any time zone, sorted.
     295         *
     296         * @since 3.3.4
     297         *
     298         * @return string[] Offsets such as '-05:00'.
     299         */
    281300        public static function get_offset_list() {
    282301                $o = wp_cache_get( 'kind_offset_list' );
    … …  
    288307                foreach ( $t_zones as $a ) {
    289308                        $datetime = new DateTime( 'now', new DateTimeZone( $a ) );
    290                         $o[]      = get_datetime_offset( $datetime );
     309                        $o[]      = Kind_Time::get_datetime_offset( $datetime );
    291310                }
    292311                $o = array_unique( $o );
    … …  
    297316
    298317        /**
    299          * Function to render date/time field inputs.
    300          *
    301          * @access public
    302          *
    303          * @param string|DateTime $time   Date/time value.
    304          * @param string $name Property name
    305          * @param array $args
    306          * @param string|array $class  Class to use for fields.
    307          * @return string
     318         * Renders date, time and offset inputs.
     319         *
     320         * @since 3.3.4
     321         *
     322         * @param array                 $args     Field arguments, as returned by validate(): 'name', 'label' and 'class'.
     323         * @param DateTime|string|false $datetime Date, or a date string. Default now.
     324         * @return string The inputs.
    308325         */
    309326        public static function field_datetime( $args, $datetime ) {
    … …  
    315332                }
    316333                $return   = array();
    317                 $time     = divide_datetime( $datetime );
     334                $time     = Kind_Time::divide_datetime( $datetime );
    318335                $return[] = sprintf( '<label for="mf2_%1$s" class="%2$s">%3$s', $args['name'], $args['class'], $args['label'] );
    319336                $return[] = sprintf( '<input type="date" name="mf2_%1$s_date" id="mf2_%1$s_date" value="%2$s"/>', $args['name'], ( $time['date'] ?? '' ) );
    … …  
    328345
    329346        /**
    330          * Function to render dateinterval field inputs.
    331          *
    332          * @access public
    333          *
    334          * @param string|Kind_DateTime $time   Date/time value.
    335          * @param string $name Property name
    336          * @param array $args
    337          * @param string|array $class  Class to use for fields.
    338          * @return string
     347         * Renders duration inputs.
     348         *
     349         * @since 3.3.4
     350         *
     351         * @param array               $args     Field arguments, as returned by validate(), including 'pieces'.
     352         * @param DateInterval|string $interval Duration, or an ISO 8601 duration. Default 0.
     353         * @return string The inputs.
    339354         */
    340355        public static function field_duration( $args, $interval ) {
    … …  
    346361                }
    347362                $return   = array();
    348                 $duration = divide_interval( $interval );
     363                $duration = Kind_Time::divide_interval( $interval );
    349364                $max      = array(
    350365                        'Y' => 1000,
    … …  
    364379
    365380        /**
    366          * Function to render author inputs.
    367          *
    368          * @access public
    369          *
    370          * @param string $prefix Field prefix.
    371          * @param string|array $author Defautl values. If string, considered to be the name property below. {
    372                  * @param string $name Author Name
    373                  * @param string $url Author URL
    374                  * @param string $photo Author Photo
    375          * }
    376          * @return string
     381         * Renders author name, URL and photo inputs.
     382         *
     383         * @since 3.3.4
     384         *
     385         * @param array       $args   Field arguments, as returned by validate().
     386         * @param array|false $author Values, with 'name', 'url' and 'photo' keys.
     387         * @return string The inputs.
    377388         */
    378389        public static function field_author( $args, $author ) {
    … …  
    394405                        ),
    395406                );
    396                 // Ensure all props are set for values
     407                // Ensure all props are set for values.
    397408                foreach ( array_keys( $props ) as $prop ) {
    398409                        if ( ! array_key_exists( $prop, $author ) ) {
    … …  
    405416
    406417        /**
    407          * Function to render a select.
    408          *
    409          * @access public
    410          *
    411          * @param string $name
    412          * @param string $selected Selected field type.
    413          * @return string
     418         * Renders a select. The options are in $args['options'].
     419         *
     420         * @since 3.3.4
     421         *
     422         * @param array  $args     Field arguments, as returned by validate().
     423         * @param string $selected Value of the selected option.
     424         * @return string The input.
    414425         */
    415426        public static function field_select( $args, $selected ) {
    … …  
    426437
    427438        /**
    428          * Function to render a url.
    429          *
    430          * @access public
    431          *
    432          * @param string $name
    433          * @param string $url
    434          * @return string
     439         * Renders a URL input.
     440         *
     441         * @since 3.3.4
     442         *
     443         * @param array  $args Field arguments, as returned by validate().
     444         * @param string $url  Value.
     445         * @return string The input.
    435446         */
    436447        public static function field_url( $args, $url ) {
    … …  
    443454
    444455        /**
    445          * Function to render a number.
    446          *
    447          * @access public
    448          *
    449          * @param string $name
    450          * @param string $value
    451          * @return string
     456         * Renders a number input. The step is in $args['step'].
     457         *
     458         * @since 3.3.4
     459         *
     460         * @param array  $args  Field arguments, as returned by validate().
     461         * @param string $value Value.
     462         * @return string The input.
    452463         */
    453464        public static function field_number( $args, $value ) {
    … …  
    460471
    461472        /**
    462          * Function to render a text.
    463          *
    464          * @access public
    465          *
    466          * @param string $name
    467          * @param string $value
    468          * @return string
     473         * Renders a text input.
     474         *
     475         * @since 3.3.4
     476         *
     477         * @param array  $args  Field arguments, as returned by validate().
     478         * @param string $value Value.
     479         * @return string The input.
    469480         */
    470481        public static function field_text( $args, $value ) {
    … …  
    477488
    478489        /**
    479          * Function to render a textarea.
    480          *
    481          * @access public
    482          *
    483          * @param string $name
    484          * @param string $value
    485          * @return string
     490         * Renders a textarea.
     491         *
     492         * @since 3.3.4
     493         *
     494         * @param array  $args  Field arguments, as returned by validate().
     495         * @param string $value Value.
     496         * @return string The input.
    486497         */
    487498        public static function field_textarea( $args, $value ) {
    … …  
    493504        }
    494505
     506        /**
     507         * Renders a textarea for a list, with items separated by semicolons.
     508         *
     509         * @since 3.3.4
     510         *
     511         * @param array        $args  Field arguments, as returned by validate().
     512         * @param string|array $value Value, or a list of values.
     513         * @return string The input.
     514         */
    495515        public static function field_list( $args, $value ) {
    496516                if ( is_array( $value ) ) {
    … …  
    500520        }
    501521
     522        /**
     523         * Renders the inputs for a venue's properties.
     524         *
     525         * @since 3.3.4
     526         *
     527         * @param array $args  Field arguments, as returned by validate(), including 'properties'.
     528         * @param array $value Values, keyed by property.
     529         * @return string The inputs.
     530         */
    502531        public static function field_venue( $args, $value ) {
    503532                return self::render( $args['properties'], $value );
    504533        }
    505534
     535        /**
     536         * Renders the inputs for a citation's properties.
     537         *
     538         * @since 3.3.4
     539         *
     540         * @param array $args  Field arguments, as returned by validate(), including 'properties'.
     541         * @param array $value Values, keyed by property.
     542         * @return string The inputs.
     543         */
    506544        public static function field_cite( $args, $value ) {
    507545                return self::render( $args['properties'], $value );
    508546        }
    509547
     548        /**
     549         * Checks whether a string starts with a prefix.
     550         *
     551         * @since 3.3.4
     552         *
     553         * @param string $source The source string.
     554         * @param string $prefix The prefix to look for.
     555         * @return bool True if the source starts with the prefix.
     556         */
    510557        public static function str_prefix( $source, $prefix ) {
    511558                return strncmp( $source, $prefix, strlen( $prefix ) ) === 0;
    512559        }
    513560
     561        /**
     562         * Returns a value from an array.
     563         *
     564         * @since 3.3.4
     565         *
     566         * @param string $key   Key.
     567         * @param array  $array The array.
     568         * @return mixed The value, or false if it is not set.
     569         */
    514570        private static function get( $key, $array ) {
    515571                if ( ! is_array( $array ) ) {
    … …  
    522578        }
    523579
     580        /**
     581         * Checks a field definition and fills in defaults.
     582         *
     583         * @since 3.3.4
     584         *
     585         * @param array $element Field definition.
     586         * @return array|false The field definition, or false if it is not valid.
     587         */
    524588        private static function validate( $element ) {
    525                 // Everything must have a type
     589                // Everything must have a type.
    526590                if ( ! self::get( 'type', $element ) ) {
    527591                        return false;
    528592                }
    529                 // Everything must have a label
     593                // Everything must have a label.
    530594                if ( ! self::get( 'label', $element ) ) {
    531595                        return false;
    532596                }
    533                 // If no name property then the label property copies over
     597                // If no name property then the label property copies over.
    534598                if ( ! self::get( 'name', $element ) ) {
    535599                        $element['name'] = sanitize_title( $element['label'] );
    536600                }
    537                 // If no label property then the name property copies over
     601                // If no label property then the name property copies over.
    538602                if ( ! self::get( 'label', $element ) ) {
    539603                        $element['label'] = $element['name'];
    540604                }
    541                 // If no class property then the class property is set to empty
     605                // If no class property then the class property is set to empty.
    542606                if ( ! self::get( 'class', $element ) ) {
    543607                        $element['class'] = '';
    544608                }
    545                 // Class can be an array
     609                // Class can be an array.
    546610                if ( is_array( $element['class'] ) ) {
    547611                        $element['class'] = implode( ' ', $element['class'] );
    548612                }
    549613                $type = self::get( 'type', $element );
    550                 // Any non supported type should be considered to be text
     614                // Any non supported type should be considered to be text.
    551615                if ( ! self::supported_type( $type ) ) {
    552616                        $type = 'text';
    553617                }
    554                 // Type Specific Conditions
     618                // Type Specific Conditions.
    555619                switch ( $type ) {
    556620                        case 'cite':
    … …  
    562626                        case 'number':
    563627                                if ( ! array_key_exists( 'step', $element ) ) {
    564                                         $element['step'] = 1; // Defaults to Even Numbers
     628                                        $element['step'] = 1; // Defaults to Even Numbers.
    565629                                }
    566630                                break;
    567631                        case 'select':
    568                                 if ( ! array_key_exists( 'options', $elements ) ) {
     632                                if ( ! array_key_exists( 'options', $element ) ) {
    569633                                        return false;
    570634                                }
    571635                                break;
    572636                        case 'duration':
    573                                 if ( array_key_exists( 'pieces', $elements ) ) {
    574                                         // Ensure only valid options
    575                                         $element['pieces'] = array_intersect( array( 'Y', 'M', 'D', 'H', 'I', 'S' ), $elements['pieces'] );
     637                                if ( array_key_exists( 'pieces', $element ) ) {
     638                                        // Ensure only valid options.
     639                                        $element['pieces'] = array_intersect( array( 'Y', 'M', 'D', 'H', 'I', 'S' ), $element['pieces'] );
    576640                                } else {
    577                                         // By default only show hours, minutes, seconds
     641                                        // By default only show hours, minutes, seconds.
    578642                                        $element['pieces'] = array( 'H', 'I', 'S' );
    579643                                }
    … …  
    582646                return $element;
    583647        }
     648
     649        /**
     650         * Whether a field type is supported.
     651         *
     652         * @since 3.3.4
     653         *
     654         * @param string $type Field type.
     655         * @return bool
     656         */
    584657        public static function supported_type( $type ) {
    585658                return in_array( $type, array( 'cite', 'venue', 'coordinate', 'author', 'datetime', 'duration', 'text', 'url', 'textarea', 'list', 'section' ), true );
    … …  
    588661
    589662        /**
    590          * Sets an array with only the mf2 prefixed meta.
    591          *
     663         * Returns the properties stored in mf2_ prefixed post meta, without the prefix.
     664         *
     665         * Reads the current post; the $post argument is not used.
     666         *
     667         * @since 3.3.4
     668         *
     669         * @param int|WP_Post $post Not used.
     670         * @return array|false The properties, or false if there is no current post.
    592671         */
    593672        private function get_mf2meta( $post ) {
    … …  
    606685                                unset( $meta[ $key ] );
    607686                                $key = str_replace( 'mf2_', '', $key );
    608                                 // Do not save microput prefixed instructions
     687                                // Do not save microput prefixed instructions.
    609688                                if ( self::str_prefix( $key, 'mp-' ) ) {
    610689                                        continue;
    … …  
    625704
    626705        /**
    627          * Function to render a form from a schema array
    628          *
    629          * @access public
    630          *
    631          * @param array $schema
    632          *
    633          * @return string Form
     706         * Renders a form from a field schema.
     707         *
     708         * @since 3.3.4
     709         *
     710         * @param array $schema Field definitions, keyed by property.
     711         * @param array $values Optional. Values, keyed by property. Default empty array.
     712         * @return string The form.
    634713         */
    635714        public static function render( $schema, $values = array() ) {
    … …  
    649728        }
    650729
    651         /* Extracts microformats elements from post data.
    652          * Microformats elements are prefixed by mf2_
    653          * After that, underscore would indicate properties that need to be reconstituted
    654         */
     730        /**
     731         * Extracts microformats properties from submitted form data.
     732         *
     733         * Properties are prefixed by mf2_. After that, an underscore marks the
     734         * pieces of a property that need to be put back together.
     735         *
     736         * @since 3.3.4
     737         *
     738         * @param array $data Form data, such as $_POST.
     739         * @return array The properties.
     740         */
    655741        public static function rebuild_data( $data ) {
    656742                $raw = array();
    … …  
    669755                                $raw[ $pieces[0] ] = $pieces[1];
    670756                                unset( $raw[ $key ] );
    671                                 // If this has the elements of a duration
     757                                // If this has the elements of a duration.
    672758                                if ( ! empty( array_intersect( array( 'Y', 'M', 'D', 'H', 'I', 'S' ), $raw[ $pieces[0] ] ) ) ) {
    673                                         $interval          = build_interval( $raw[ $pieces[0] ] );
     759                                        $interval          = Kind_Time::build_interval( $raw[ $pieces[0] ] );
    674760                                        $raw[ $pieces[0] ] = date_interval_to_iso8601( $interval );
    675761                                }
    676                                 // If this has the elements of a datetime
     762                                // If this has the elements of a datetime.
    677763                                if ( ! empty( array_intersect( array( 'date', 'time', 'offset' ), $raw[ $pieces[0] ] ) ) ) {
    678                                         $datetime          = build_datetime( $raw[ $pieces[0] ] );
     764                                        $datetime          = Kind_Time::build_datetime( $raw[ $pieces[0] ] );
    679765                                        $raw[ $pieces[0] ] = $datetime->format( DATE_W3C );
    680766                                }
  • indieweb-post-kinds/trunk/includes/class-kind-media-metadata.php

    r3001401 r3738687  
    11<?php
    22/**
     3 * Kind_Media_Metadata class.
    34 *
    4  * Enhances Metadata for Media
     5 * @package Post_Kinds
     6 * @since   3.1.0
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Enhances the metadata stored for media attachments, and records the media a post uses.
     13 *
     14 * @since 3.1.0
    515 */
    616class Kind_Media_Metadata {
    … …  
    919         * Function to Initialize the Configuration.
    1020         *
     21         * @since 3.1.0
     22         *
    1123         * @access public
    1224         */
    1325        public static function init() {
    14                 $wp_version = get_bloginfo( 'version' );
    1526                add_filter( 'wp_generate_attachment_metadata', array( static::class, 'wp_generate_attachment_metadata' ), 33, 2 );
    16                 if ( version_compare( $wp_version, '5.3', '>' ) ) {
    17                         add_filter( 'wp_update_attachment_metadata', array( static::class, 'wp_sanitize_media_metadata' ), 9, 2 );
    18                 }
     27                add_filter( 'wp_update_attachment_metadata', array( static::class, 'wp_sanitize_media_metadata' ), 9, 2 );
    1928                add_action( 'wp_enqueue_scripts', array( static::class, 'enqueue' ) );
    2029
    21                 add_action( 'save_post', array( static::class, 'save_post' ), 20 );
     30                // Kinds apply to posts only; revisions and autosaves have their own post type.
     31                add_action( 'save_post_post', array( static::class, 'save_post' ), 20 );
     32
     33                // The photo property can change outside a save, for example from Micropub.
     34                foreach ( array( 'added_post_meta', 'updated_post_meta', 'deleted_post_meta' ) as $hook ) {
     35                        add_action( $hook, array( static::class, 'photo_meta_changed' ), 10, 3 );
     36                }
    2237
    2338                add_filter( 'attachment_fields_to_edit', array( static::class, 'attachment_fields_to_edit' ), 10, 2 );
    … …  
    2540        }
    2641
     42        /**
     43         * Whether the request is for an AMP page, according to the AMP plugin.
     44         *
     45         * @since 3.5.1
     46         *
     47         * @return bool
     48         */
    2749        public static function is_amp_endpoint() {
    2850                if ( function_exists( 'is_amp_endpoint' ) ) {
    … …  
    3254        }
    3355
     56        /**
     57         * Enqueues the media fragment script on single posts that have media.
     58         *
     59         * @since 3.3.0
     60         */
    3461        public static function enqueue() {
    35                 if ( ! is_front_page() && is_singular() && ! self::is_amp_endpoint() ) {
    36                         wp_enqueue_script(
    37                                 'media-fragment',
    38                                 plugins_url( 'js/clone-media-fragment.js', __DIR__ ),
    39                                 array(),
    40                                 '1.0',
    41                                 true
    42                         );
    43                 }
     62                if ( is_front_page() || ! is_singular() || self::is_amp_endpoint() || ! self::has_media( get_queried_object() ) ) {
     63                        return;
     64                }
     65                wp_enqueue_script(
     66                        'media-fragment',
     67                        plugins_url( 'js/clone-media-fragment.js', __DIR__ ),
     68                        array(),
     69                        '1.0',
     70                        // WordPress 6.2 reads the array as a true $in_footer; 6.3 and later also defer it.
     71                        array(
     72                                'in_footer' => true,
     73                                'strategy'  => 'defer',
     74                        )
     75                );
     76        }
     77
     78        /**
     79         * Returns whether a post may show audio or video, which the media
     80         * fragment script links to.
     81         *
     82         * @since 4.0.0
     83         *
     84         * @param mixed $post Post object, usually the queried object.
     85         * @return bool True if the post has an audio or video kind or property, or
     86         *              audio or video in its content.
     87         */
     88        public static function has_media( $post ) {
     89                if ( ! $post instanceof WP_Post ) {
     90                        return false;
     91                }
     92                if ( 'post' === $post->post_type ) {
     93                        if ( has_post_kind( array( 'audio', 'video', 'listen', 'watch', 'jam' ), $post ) ) {
     94                                return true;
     95                        }
     96                        if ( get_post_meta( $post->ID, 'mf2_audio', true ) || get_post_meta( $post->ID, 'mf2_video', true ) ) {
     97                                return true;
     98                        }
     99                }
     100                return (bool) preg_match( '/<(audio|video)[\s>]|\[(audio|video|playlist)[\s\]]/i', $post->post_content );
    44101        }
    45102
    … …  
    47104        /**
    48105         * Displays the Media Creator Description on the Attachment Page.
     106         *
     107         * @since 3.4.0
    49108         *
    50109         * @param array   $form_fields See attachment_fields_to_edit filter in WordPress.
    … …  
    55114
    56115                $author = get_post_meta( $post->ID, 'mf2_author', true );
    57                 if ( Parse_This_MF2_Utils::is_microformat( $author ) ) {
    58                         $author = mf2_to_jf2( $author );
     116                if ( \ParseThis\MF2_Utils::is_microformat( $author ) ) {
     117                        $author = \ParseThis\mf2_to_jf2( $author );
    59118                } else {
    60119                        $author = array();
    … …  
    81140         * Saves Extra Fields.
    82141         *
    83          * @param array $post Attachment post data.
    84          * @param array   $attachment data.
     142         * @since 3.4.0
     143         *
     144         * @param array $post       Attachment post data.
     145         * @param array $attachment Attachment fields submitted.
    85146         */
    86147        public static function attachment_fields_to_save( $post, $attachment ) {
     148                // Core checks this before saving; check again as the filter can be applied elsewhere.
     149                if ( empty( $post['ID'] ) || ! current_user_can( 'edit_post', $post['ID'] ) ) {
     150                        return $post;
     151                }
     152                // Core passes the request data slashed.
    87153                $author = array();
    88                 if ( isset( $attachment['author_name'] ) && ! empty( $attachment['author_name'] ) ) {
    89                         $author['name'] = $attachment['author_name'];
    90                 }
    91                 if ( isset( $attachment['author_url'] ) && ! empty( $attachment['author_url'] ) ) {
    92                         $author['url'] = $attachment['author_url'];
     154                if ( isset( $attachment['author_name'] ) && is_string( $attachment['author_name'] ) ) {
     155                        $author['name'] = sanitize_text_field( wp_unslash( $attachment['author_name'] ) );
     156                }
     157                if ( isset( $attachment['author_url'] ) && is_string( $attachment['author_url'] ) ) {
     158                        $author['url'] = sanitize_url( trim( wp_unslash( $attachment['author_url'] ) ), array( 'http', 'https' ) );
    93159                }
    94160                $author = array_filter( $author );
    … …  
    97163                } else {
    98164                        $author['type'] = 'card';
    99                         update_post_meta( $post['ID'], 'mf2_author', jf2_to_mf2( $author ) );
     165                        // The metadata API unslashes, so slash the sanitized values.
     166                        update_post_meta( $post['ID'], 'mf2_author', wp_slash( \ParseThis\jf2_to_mf2( $author ) ) );
    100167                }
    101168                return $post;
    102169        }
    103170
    104         /*
     171        /**
     172         * Finds the media library attachments used in a content block.
     173         *
     174         * The content is parsed once for images, audio and video. An image's ID
     175         * is taken from its wp-image-{id} class when it has one; otherwise, and
     176         * for audio and video sources, the URL is looked up.
     177         *
     178         * @since 4.0.0
     179         *
     180         * @param string $content Content.
     181         * @return array {
     182         *     Attachment IDs found, each list without duplicates or zeros.
     183         *
     184         *     @type int[] $img   Image attachment IDs.
     185         *     @type int[] $audio Audio attachment IDs.
     186         *     @type int[] $video Video attachment IDs.
     187         * }
     188         */
     189        private static function get_media_from_content( $content ) {
     190                $return = array(
     191                        'img'   => array(),
     192                        'audio' => array(),
     193                        'video' => array(),
     194                );
     195                $content = wp_unslash( (string) $content );
     196                // Most content has no media; skip parsing it.
     197                if ( false === stripos( $content, '<img' ) && false === stripos( $content, '<audio' ) && false === stripos( $content, '<video' ) ) {
     198                        return $return;
     199                }
     200                $doc = \ParseThis\pt_load_domdocument( $content );
     201
     202                foreach ( $doc->getElementsByTagName( 'img' ) as $image ) {
     203                        $id = 0;
     204                        foreach ( explode( ' ', $image->getAttribute( 'class' ) ) as $class ) {
     205                                if ( 0 === strpos( $class, 'wp-image-' ) ) {
     206                                        $id = (int) substr( $class, strlen( 'wp-image-' ) );
     207                                        break;
     208                                }
     209                        }
     210                        if ( ! $id ) {
     211                                $id = attachment_url_to_postid( $image->getAttribute( 'src' ) );
     212                        }
     213                        $return['img'][] = $id;
     214                }
     215                foreach ( array( 'audio', 'video' ) as $tag ) {
     216                        foreach ( $doc->getElementsByTagName( $tag ) as $media ) {
     217                                foreach ( $media->getElementsByTagName( 'source' ) as $source ) {
     218                                        $return[ $tag ][] = attachment_url_to_postid( remove_query_arg( '_', $source->getAttribute( 'src' ) ) );
     219                                }
     220                        }
     221                }
     222                foreach ( $return as $type => $ids ) {
     223                        $return[ $type ] = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) );
     224                }
     225                return $return;
     226        }
     227
     228        /**
    105229         * Determine Attached Images from a Content Block.
     230         *
     231         * @since 3.4.0
    106232         *
    107233         * @param string $content Content.
    108234         * @return array Array of Attachment IDs.
    109         */
     235         */
    110236        public static function get_img_from_content( $content ) {
    111                 $content = wp_unslash( $content );
    112                 $return  = array();
    113                 $doc     = pt_load_domdocument( $content );
    114                 $images  = $doc->getElementsByTagName( 'img' );
    115                 foreach ( $images as $image ) {
    116                         $classes = $image->getAttribute( 'class' );
    117                         $classes = explode( ' ', $classes );
    118                         foreach ( $classes as $class ) {
    119                                 if ( 0 === strpos( $class, 'wp-image-' ) ) {
    120                                         $id = (int) str_replace( 'wp-image-', '', $class );
    121                                         if ( 0 !== $id ) {
    122                                                 $return[] = $id;
    123                                         }
    124                                         break;
    125                                 }
    126                         }
    127                         $url = $image->getAttribute( 'src' );
    128                         $id  = attachment_url_to_postid( $url );
    129                         if ( 0 !== $id ) {
    130                                 $return[] = $id;
    131                         }
    132                 }
    133                 return array_unique( $return );
    134         }
    135 
    136         /*
     237                return self::get_media_from_content( $content )['img'];
     238        }
     239
     240        /**
    137241         * Determine Attached Audio from a Content Block.
     242         *
     243         * @since 3.4.0
    138244         *
    139245         * @param string $content Content.
    140246         * @return array Array of Attachment IDs.
    141         */
     247         */
    142248        public static function get_audio_from_content( $content ) {
    143                 $content = wp_unslash( $content );
    144                 $return  = array();
    145                 $doc     = pt_load_domdocument( $content );
    146                 $audios  = $doc->getElementsByTagName( 'audio' );
    147                 foreach ( $audios as $audio ) {
    148                         $sources = $audio->getElementsByTagName( 'source' );
    149                         foreach ( $sources as $source ) {
    150                                 $url      = remove_query_arg( '_', $source->getAttribute( 'src' ) );
    151                                 $id       = attachment_url_to_postid( $url );
    152                                 $return[] = $id;
    153                         }
    154                 }
    155                 return array_unique( $return );
    156         }
    157 
    158         /*
     249                return self::get_media_from_content( $content )['audio'];
     250        }
     251
     252        /**
    159253         * Determine Attached Videos from a Content Block.
     254         *
     255         * @since 3.4.0
    160256         *
    161257         * @param string $content Content.
    162258         * @return array Array of Attachment IDs.
    163         */
     259         */
    164260        public static function get_video_from_content( $content ) {
    165                 $content = wp_unslash( $content );
    166                 $return  = array();
    167                 $doc     = pt_load_domdocument( $content );
    168                 $videos  = $doc->getElementsByTagName( 'video' );
    169                 foreach ( $videos as $video ) {
    170                         $sources = $video->getElementsByTagName( 'source' );
    171                         foreach ( $sources as $source ) {
    172                                 $url      = remove_query_arg( '_', $source->getAttribute( 'src' ) );
    173                                 $id       = attachment_url_to_postid( $url );
    174                                 $return[] = $id;
    175                         }
    176                 }
    177                 return array_unique( $return );
    178         }
    179 
    180         private function media_sideload_image( $url, $post_id, $description = null ) {
    181                 // To prevent sideloading the same URL multiple times check for the original URL which will now be stored
    182                 $ids = get_posts(
    183                         array(
    184                                 'post_type'        => 'attachment',
    185                                 'suppress_filters' => false,
    186                                 'nopaging'         => true,
    187                                 'meta_key'         => '_source_url',
    188                                 'meta_value'       => $url,
    189                                 'fields'           => 'ids',
    190                         )
    191                 );
    192                 if ( ! empty( $ids ) ) {
    193                         return $ids[0];
    194                 }
    195                 $id = media_sideload_image( $url, $post_id, $description, 'id' );
    196                 // Version 5.4 adds this automatically.
    197                 if ( $id && version_compare( $wp_version, '5.3', '>' ) ) {
    198                         update_post_meta( $id, '_source_url', $url );
    199                 }
    200                 return $id;
    201         }
    202 
    203 
    204 
    205         /**
    206          * Every time the post is saved check for media embedded in content and save a list of attachment IDs.
    207         */
     261                return self::get_media_from_content( $content )['video'];
     262        }
     263
     264        /**
     265         * Every time a post is saved, stores the attachment IDs of the media in its content.
     266         *
     267         * The lists are kept in the _content_img_ids, _content_audio_ids and
     268         * _content_video_ids meta; a key is deleted when there is no media of that
     269         * type, so the photos archive can select posts by whether the key exists.
     270         *
     271         * @since 3.4.0
     272         *
     273         * @param int $post_id Post ID.
     274         */
    208275        public static function save_post( $post_id ) {
    209                 $post    = get_post( $post_id );
    210                 $content = do_shortcode( $post->post_content );
    211 
    212                 $gallery_ids = array();
    213                 $gallery     = get_post_gallery( $post_id, false );
    214 
    215                 if ( is_array( $gallery ) ) {
    216                         if ( array_key_exists( 'ids', $gallery ) ) {
    217                                 $gallery_ids = explode( ',', $gallery['ids'] );
    218                         }
    219                 }
    220                 $ids = self::get_img_from_content( $content );
    221                 if ( is_array( $ids ) && is_array( $gallery_ids ) ) {
    222                         $ids = array_merge( $ids, $gallery_ids );
    223                 }
    224 
    225                 if ( ! $ids ) {
    226                         delete_post_meta( $post_id, '_content_img_ids' );
     276                // The classic editor's draft autosave; the next save will record the media.
     277                if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
     278                        return;
     279                }
     280                $post = get_post( $post_id );
     281                if ( ! $post ) {
     282                        return;
     283                }
     284                $content = $post->post_content;
     285                // Shortcodes such as [gallery], [audio] and [video] produce the media markup.
     286                if ( false !== strpos( $content, '[' ) ) {
     287                        $content = do_shortcode( $content );
     288                }
     289                $media = self::get_media_from_content( $content );
     290
     291                $gallery = get_post_gallery( $post_id, false );
     292                if ( is_array( $gallery ) && ! empty( $gallery['ids'] ) ) {
     293                        $media['img'] = array_values( array_unique( array_merge( $media['img'], array_filter( array_map( 'intval', explode( ',', $gallery['ids'] ) ) ) ) ) );
     294                }
     295
     296                foreach ( $media as $type => $ids ) {
     297                        $key = '_content_' . $type . '_ids';
     298                        if ( empty( $ids ) ) {
     299                                delete_post_meta( $post_id, $key );
     300                        } else {
     301                                update_post_meta( $post_id, $key, $ids );
     302                        }
     303                }
     304                self::update_photo_flag( $post_id );
     305        }
     306
     307        /**
     308         * Updates the photo flag when a post's photo property changes.
     309         *
     310         * @since 4.0.0
     311         *
     312         * @param int|int[] $meta_id   Meta ID, or IDs when deleting.
     313         * @param int       $object_id Post ID.
     314         * @param string    $meta_key  Meta key.
     315         */
     316        public static function photo_meta_changed( $meta_id, $object_id, $meta_key ) {
     317                if ( 'mf2_photo' === $meta_key && 'post' === get_post_type( $object_id ) ) {
     318                        self::update_photo_flag( $object_id );
     319                }
     320        }
     321
     322        /**
     323         * Records whether a post has photos, for the photos archive.
     324         *
     325         * A post has photos when it has images in its content or a photo
     326         * property. The _kind_has_photo meta exists only for those posts, so the
     327         * archive can select them with one indexed meta key.
     328         *
     329         * @since 4.0.0
     330         *
     331         * @param int $post_id Post ID.
     332         */
     333        public static function update_photo_flag( $post_id ) {
     334                if ( get_post_meta( $post_id, '_content_img_ids', true ) || get_post_meta( $post_id, 'mf2_photo', true ) ) {
     335                        update_post_meta( $post_id, '_kind_has_photo', 1 );
    227336                } else {
    228                         update_post_meta( $post_id, '_content_img_ids', $ids );
    229                 }
    230                 $ids = self::get_video_from_content( $content );
    231                 if ( ! $ids ) {
    232                         delete_post_meta( $post_id, '_content_video_ids' );
    233                 } else {
    234                         update_post_meta( $post_id, '_content_video_ids', $ids );
    235                 }
    236                 $ids = self::get_audio_from_content( $content );
    237                 if ( ! $ids ) {
    238                         delete_post_meta( $post_id, '_content_audio_ids' );
    239                 } else {
    240                         update_post_meta( $post_id, '_content_audio_ids', $ids );
     337                        delete_post_meta( $post_id, '_kind_has_photo' );
    241338                }
    242339        }
    … …  
    248345         * bad character encodings from causing database calls and API endpoints to fail.
    249346         *
    250          * @param array $metadata An existing array with data
     347         * @since 3.3.0
     348         *
     349         * @param array $metadata An existing array with data.
    251350         *
    252351         * @return array Returns array of sanitized metadata.
    … …  
    271370        }
    272371
     372        /**
     373         * Stores an attachment's author, album and duration from its file metadata.
     374         *
     375         * The image credit, or the artist of audio or video, becomes the author;
     376         * the album becomes the publication.
     377         *
     378         * @since 3.3.4
     379         *
     380         * @param array|mixed $data          Attachment metadata.
     381         * @param int         $attachment_id Attachment ID.
     382         * @return array|mixed The metadata, with empty values removed.
     383         */
    273384        public static function wp_generate_attachment_metadata( $data, $attachment_id ) {
    274385                if ( ! is_array( $data ) ) {
    275386                        return $data;
    276387                }
    277                 $data       = array_filter( $data );
    278                 $attachment = get_post( $attachment_id, ARRAY_A );
     388                $data = array_filter( $data );
    279389                if ( isset( $data['image_meta'] ) ) {
    280390                        $meta = $data['image_meta'];
    … …  
    283393                                        $attachment_id,
    284394                                        'mf2_author',
    285                                         jf2_to_mf2(
     395                                        \ParseThis\jf2_to_mf2(
    286396                                                array(
    287397                                                        'name' => $meta['credit'],
    … …  
    299409                                $attachment_id,
    300410                                'mf2_author',
    301                                 jf2_to_mf2(
     411                                \ParseThis\jf2_to_mf2(
    302412                                        array(
    303413                                                'name' => $data['artist'],
    … …  
    308418                }
    309419                if ( isset( $data['length'] ) ) {
    310                         update_post_meta( $attachment_id, 'mf2_duration', array( seconds_to_iso8601( $data['length'] ) ) );
     420                        update_post_meta( $attachment_id, 'mf2_duration', array( \ParseThis\seconds_to_iso8601( $data['length'] ) ) );
    311421                }
    312422                return $data;
  • indieweb-post-kinds/trunk/includes/class-kind-media-view.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Kind Media View Class
     3 * Kind_Media_View class.
    44 *
    5  * @package Post Kind
    6  * Used to Display Media
     5 * @package Post_Kinds
     6 * @since   3.3.0
    77 */
    88
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Builds the markup for images, audio and video in kind views.
     13 *
     14 * @since 3.3.0
     15 */
    916class Kind_Media_View {
     17        /**
     18         * Attachment IDs or media URLs.
     19         *
     20         * @since 3.3.0
     21         * @var int[]|string[]|int|string
     22         */
    1023        private $ids;
     24
     25        /**
     26         * Media type: 'photo', 'audio' or 'video'.
     27         *
     28         * @since 3.3.0
     29         * @var string
     30         */
    1131        private $type;
     32
     33        /**
     34         * Sets up the media to display.
     35         *
     36         * @since 3.3.0
     37         *
     38         * @param int[]|string[]|int|string $ids  Attachment IDs or media URLs.
     39         * @param string                    $type Media type: 'photo', 'audio' or 'video'.
     40         */
    1241        public function __construct( $ids, $type ) {
    1342                $this->ids  = $ids;
    … …  
    1544        }
    1645
     46        /**
     47         * Returns the markup for the media.
     48         *
     49         * @since 3.3.0
     50         *
     51         * @return string The markup, or an empty string if the type is not supported.
     52         */
    1753        public function get() {
    1854                switch ( $this->type ) {
    … …  
    2763        }
    2864
     65        /**
     66         * Returns a one-column gallery of photos.
     67         *
     68         * @since 3.3.0
     69         *
     70         * @param int[] $photos Attachment IDs.
     71         * @return string The gallery markup.
     72         */
    2973        private function photo( $photos ) {
    3074                return gallery_shortcode(
    … …  
    4185         * Return a media view for the audio post kind.
    4286         *
     87         * @since 3.3.0
     88         *
    4389         * @access private
    44          * @param int|string   $id   Audio attachment ID or audio URL
    45          * @param mixed $args Arguments for the audio media view.
     90         *
     91         * @param int|string|array $id   Audio attachment ID or audio URL, or a list of them.
     92         * @param array|null       $args Arguments for the audio media view.
    4693         *
    4794         * @return array|string|void
    … …  
    72119                $return = '';
    73120                if ( $args['src'] ) {
     121                        $args   = wp_parse_args( $args, $default );
    74122                        $return = wp_audio_shortcode( $args );
    75123                }
    … …  
    80128         * Return a media view for the video post kind.
    81129         *
     130         * @since 3.3.0
     131         *
    82132         * @access private
    83          * @param int|string   $id   Video attachment ID or URL.
    84          * @param mixed $args Arguments for the video media view.
     133         *
     134         * @param int|string|array $id   Video attachment ID or URL, or a list of them.
     135         * @param array|null       $args Arguments for the video media view.
    85136         *
    86137         * @return array|string|void
    … …  
    98149                } elseif ( wp_http_validate_url( $id ) ) {
    99150                        $args['src'] = $id;
    100                 } if ( 0 === $id ) {
     151                } elseif ( 0 === $id ) {
    101152                        return '';
    102153                } else {
  • indieweb-post-kinds/trunk/includes/class-kind-menu-widget.php

    r3067251 r3738687  
    11<?php
     2/**
     3 * Kind_Menu_Widget class.
     4 *
     5 * @package Post_Kinds
     6 * @since   3.1.3
     7 */
    28
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Widget that lists links to the archive of each kind.
     13 *
     14 * @since 3.1.3
     15 */
    316class Kind_Menu_Widget extends WP_Widget {
    417        /**
    518         * Register widget with WordPress.
     19         *
     20         * @since 3.1.3
    621         */
    722        public function __construct() {
    823                parent::__construct(
    9                         'Kind_Menu_Widget',                // Base ID
    10                         __( 'Kind Menu Widget', 'indieweb-post-kinds' ),        // Name
     24                        'Kind_Menu_Widget',                // Base ID.
     25                        __( 'Kind Menu Widget', 'indieweb-post-kinds' ),        // Name.
    1126                        array(
    1227                                'classname'             => 'kind_menu_widget',
    … …  
    1934        /**
    2035         * Front-end display of widget.
     36         *
     37         * @since 3.1.3
    2138         *
    2239         * @see WP_Widget::widget()
    … …  
    3552                }
    3653                $include = array_merge( $include, array( 'note', 'reply', 'article' ) );
    37                 // Filter Kinds
     54                /**
     55                 * This filter is documented in includes/class-kind-taxonomy.php
     56                 */
    3857                $include = array_unique( apply_filters( 'kind_include', $include ) );
    39                 // Note cannot be removed or disabled without hacking the code
     58                // Note cannot be removed or disabled without hacking the code.
    4059                if ( ! in_array( 'note', $include, true ) ) {
    4160                        $include[] = 'note';
    4261                }
    4362
    44                 // phpcs:ignore
    45                 echo $args['before_widget'];
     63                echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper markup registered by the theme.
    4664                if ( $title ) {
    47                         echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore
     65                        echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Wrapper markup is the theme's; the title is escaped by core's widget_title filter.
    4866                }
    4967                ?>
    … …  
    5270                <ul>
    5371                <?php
    54                 $feed = is_front_page() ? 'rel="feed"' : '';
     72                $feed = is_front_page() ? ' rel="feed"' : '';
    5573                foreach ( $include as $i ) {
    5674                        $count = Kind_Taxonomy::get_post_kind_count( $i );
    … …  
    6078                        $name = ( 1 === $count ) ? Kind_Taxonomy::get_kind_info( $i, 'singular_name' ) : Kind_Taxonomy::get_kind_info( $i, 'name' );
    6179                        /* translators: 1. Blog Title 2. Kind Singular Name */
    62                         $title = sprintf( __( 'title="%1$s %2$s Feed"', 'indieweb-post-kinds' ), esc_attr( get_bloginfo( 'name' ) ), esc_attR( Kind_Taxonomy::get_kind_info( $i, 'singular_name' ) ) );
    63                         if ( 1 === (int) $instance['count'] ) {
    64                                 $count = sprintf( '%1$s (%2$s)', $name, $count );
     80                        $feed_title = sprintf( __( '%1$s %2$s Feed', 'indieweb-post-kinds' ), get_bloginfo( 'name' ), Kind_Taxonomy::get_kind_info( $i, 'singular_name' ) );
     81                        if ( 1 === (int) ( $instance['count'] ?? 0 ) ) {
     82                                $label = sprintf( '%1$s (%2$s)', $name, number_format_i18n( $count ) );
    6583                        } else {
    66                                 $count = $name;
     84                                $label = $name;
    6785                        }
    6886                        printf(
    69                                 '<li><a %4$s %5$s type="text/html" href="%2$s">%1$s%3$s</a></li>',
    70                                 Kind_Taxonomy::get_icon( $i ), // phpcs:ignore
     87                                '<li><a%4$s title="%5$s" type="text/html" href="%2$s">%1$s%3$s</a></li>',
     88                                Kind_Taxonomy::get_icon( $i ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon markup.
    7189                                esc_url( Kind_Taxonomy::get_post_kind_link( $i ) ),
    72                                 $count, // phpcs:ignore
    73                                 $feed, // phpcs:ignore
    74                                 $title  // phpcs:ignore
    75                         ); // phpcs:ignore
     90                                esc_html( $label ),
     91                                $feed, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- A fixed attribute string.
     92                                esc_attr( $feed_title )
     93                        );
    7694                }
    77                 if ( 1 === (int) $instance['all'] ) {
    78                         printf( '<li><a type="text/html" href="%2$s">%1$s%3$s</a></li>', Kind_Taxonomy::get_icon( 'firehose' ), esc_url( get_post_type_archive_link( 'post' ) ), esc_html__( 'All Posts', 'indieweb-post-kinds' ) ); // phpcs:ignore
     95                if ( 1 === (int) ( $instance['all'] ?? 0 ) ) {
     96                        printf( '<li><a type="text/html" href="%2$s">%1$s%3$s</a></li>', Kind_Taxonomy::get_icon( 'firehose' ), esc_url( get_post_type_archive_link( 'post' ) ), esc_html__( 'All Posts', 'indieweb-post-kinds' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon markup.
    7997                }
    8098                ?>
    … …  
    83101
    84102                <?php
    85                 // phpcs:ignore
    86                 echo $args['after_widget'];
     103                echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper markup registered by the theme.
    87104        }
    88105
    … …  
    90107        /**
    91108         * Sanitize widget form values as they are saved.
     109         *
     110         * @since 3.1.3
    92111         *
    93112         * @see WP_Widget::update()
    … …  
    99118         */
    100119        public function update( $new_instance, $old_instance ) {
    101                 array_walk_recursive( $new_instance, 'sanitize_text_field' );
    102                 return $new_instance;
     120                $termslist = isset( $new_instance['termslist'] ) && is_array( $new_instance['termslist'] ) ? array_filter( $new_instance['termslist'], 'is_string' ) : array();
     121                $termslist = array_values( array_unique( array_intersect( array_map( 'sanitize_key', $termslist ), Kind_Taxonomy::get_kind_list() ) ) );
     122                return array(
     123                        'title'     => isset( $new_instance['title'] ) && is_string( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '',
     124                        'termslist' => $termslist,
     125                        'count'     => empty( $new_instance['count'] ) ? 0 : 1,
     126                        'all'       => empty( $new_instance['all'] ) ? 0 : 1,
     127                );
    103128        }
    104129
    … …  
    106131        /**
    107132         * Create the form for the Widget admin
     133         *
     134         * @since 3.1.3
    108135         *
    109136         * @see WP_Widget::form()
    … …  
    120147                $termslist = (array) $instance['termslist'];
    121148                ?>
    122                                 <p><label for="title"><?php esc_html_e( 'Title: ', 'indieweb-post-kinds' ); ?></label>
    123                                 <input type="text" size="30" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?> id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" value="
    124                 <?php echo esc_html( ( $instance['title'] ?? '' ) ); ?>" /></p>
     149                                <p><label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title: ', 'indieweb-post-kinds' ); ?></label>
     150                                <input type="text" size="30" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" value="<?php echo esc_attr( $instance['title'] ?? '' ); ?>" /></p>
    125151                <div id="kind-all">
    126152                <?php
    127                 foreach ( get_option( 'kind_termslist', Kind_Taxonomy::get_kind_list() ) as $term ) {
     153                foreach ( Kind_Config::get_termslist() as $term ) {
    128154                        $value = Kind_Taxonomy::get_post_kind_info( $term );
    129155                        if ( $value->show ) {
    … …  
    135161                                        checked( in_array( $term, $termslist, true ), true, false )
    136162                                );
    137                                 printf( '%1$s %2$s<br />', Kind_Taxonomy::get_icon( $term ), esc_html( $value->singular_name ) ); //phpcs:ignore
     163                                printf( '%1$s %2$s<br />', Kind_Taxonomy::get_icon( $term ), esc_html( $value->singular_name ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon markup.
    138164                        }
    139165                }
  • indieweb-post-kinds/trunk/includes/class-kind-metabox.php

    r3001401 r3738687  
    11<?php
    22/**
    3  * Post Kind Post MetaBox Class
     3 * Kind_Metabox class.
    44 *
    5  * Sets Up Tabbed Metabox in the Posting UI for Kind data.
     5 * @package Post_Kinds
     6 * @since   2.2.0
    67 */
    78
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Adds the tabbed Post Kinds metabox to the Classic Editor and saves its fields.
     13 *
     14 * @since 2.2.0
     15 */
    816class Kind_Metabox {
    917
    1018        /**
     19         * Plugin version, used to version enqueued assets.
     20         *
    1121         * @var string $version
    1222         */
    … …  
    1525        /**
    1626         * Function to initiate our metabox.
     27         *
     28         * @since 2.2.0
    1729         *
    1830         * @access public
    … …  
    2133                self::$version = Post_Kinds_Plugin::$version;
    2234                add_action( 'edit_form_after_title', array( static::class, 'after_title_metabox' ) );
    23                 // Add meta box to new post/post pages only
     35                // Add meta box to new post/post pages only.
    2436                add_action( 'load-post.php', array( static::class, 'kindbox_setup' ) );
    2537                add_action( 'load-post-new.php', array( static::class, 'kindbox_setup' ) );
    … …  
    3345         * Function to change our post kind.
    3446         *
     47         * @since 3.1.0
     48         *
    3549         * @access public
    3650         *
    … …  
    6882
    6983        /**
    70          * Function to change if a new post should be considered empty.
     84         * Filters whether a post should be considered empty.
     85         *
     86         * Many kinds (likes, bookmarks, reposts, and so on) legitimately have no
     87         * content, title or excerpt, which core would otherwise reject as empty.
     88         *
     89         * @since 3.0.0
    7190         *
    7291         * @access public
    7392         *
    7493         * @param bool  $maybe_empty Whether or not the post should be considered empty.
    75          * @param array $postarr     Data for the post to be inserted.
     94         * @param array $postarr     Data for the post to be inserted or updated.
    7695         *
    7796         * @return bool
    7897         */
    7998        public static function wp_insert_post_empty_content( $maybe_empty, $postarr ) {
    80                 // Always let updates to trash posts through
    81                 if ( 'trash' === $postarr['post_status'] ) {
     99                // Always let updates to trash posts through.
     100                if ( isset( $postarr['post_status'] ) && 'trash' === $postarr['post_status'] ) {
    82101                        return false;
    83102                }
    84                 // Let All Micropub Posts through
    85                 if ( isset( $postarr['meta_input'] ) && isset( $postarr['meta_input']['micropub_auth_response'] ) ) {
     103                // Let all Micropub creates through.
     104                if ( isset( $postarr['meta_input']['micropub_auth_response'] ) ) {
    86105                        return false;
    87106                }
    88                 if ( ! isset( $postarr['tax_input'] ) && ! isset( $postarr['tax_input']['kind'] ) ) {
    89                         return $maybe_empty;
    90                 }
    91                 $kind = get_term_by( 'id', $postarr['tax_input']['kind'][0], 'kind' );
    92                 $kind = ( $kind instanceof WP_Term ) ? $kind->slug : '';
    93                 // Use traditional rules for articles
     107                $kind = self::kind_from_postarr( $postarr );
     108                // Use traditional rules for articles, and for posts without a kind.
    94109                if ( 'article' === $kind || ! $kind ) {
    95110                        return $maybe_empty;
    96111                }
    97                 $keys = array( 'cite_url', 'cite_name', 'cite_summary' );
    98                 $keys = array_flip( $keys );
    99                 $diff = array_filter( array_intersect_key( $_POST, $keys ) );
    100                 if ( ! empty( $diff ) ) {
     112                // An existing post of a kind other than article may have no content,
     113                // for example when Micropub updates a like.
     114                if ( ! empty( $postarr['ID'] ) ) {
    101115                        return false;
    102116                }
     117                // A new post from the metabox needs something to respond to.
     118                foreach ( array( 'cite_url', 'cite_name', 'cite_summary' ) as $key ) {
     119                        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Only checks whether a field was filled in; save_post() verifies the nonce before saving.
     120                        if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) && '' !== sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) ) {
     121                                return false;
     122                        }
     123                }
    103124                return $maybe_empty;
    104125        }
    105126
    106127        /**
     128         * Returns the kind slug for post data being inserted or updated.
     129         *
     130         * The kind comes from tax_input, which the kind metabox submits as a slug and
     131         * other callers may pass as term IDs, or failing that from the existing post.
     132         *
     133         * @since 4.0.0
     134         *
     135         * @access public
     136         *
     137         * @param array $postarr Data for the post to be inserted or updated.
     138         * @return string Kind slug, or an empty string if none.
     139         */
     140        public static function kind_from_postarr( $postarr ) {
     141                if ( isset( $postarr['tax_input']['kind'] ) ) {
     142                        $kind = $postarr['tax_input']['kind'];
     143                        if ( is_array( $kind ) ) {
     144                                $kind = reset( $kind );
     145                        }
     146                        $term = is_numeric( $kind ) ? get_term_by( 'id', (int) $kind, 'kind' ) : get_term_by( 'slug', (string) $kind, 'kind' );
     147                        return ( $term instanceof WP_Term ) ? $term->slug : '';
     148                }
     149                if ( ! empty( $postarr['ID'] ) ) {
     150                        $kind = get_post_kind_slug( $postarr['ID'] );
     151                        return $kind ? $kind : '';
     152                }
     153                return '';
     154        }
     155
     156        /**
    107157         * Execute metaboxes for the current screen, after the post title.
    108158         *
     159         * @since 3.0.0
     160         *
    109161         * @access public
    110162         * @param WP_Post $post Post object for the current screen.
    … …  
    117169        /**
    118170         * Metabox setup.
     171         *
     172         * @since 2.2.0
    119173         *
    120174         * @access public
    … …  
    124178                /* Add meta boxes on the 'add_meta_boxes' hook. */
    125179                add_action( 'add_meta_boxes', array( $cls, 'add_meta_boxes' ) );
     180                add_filter( 'hidden_meta_boxes', array( $cls, 'hidden_meta_boxes' ), 10, 2 );
    126181                add_action( 'admin_enqueue_scripts', array( $cls, 'enqueue_admin_scripts' ) );
    127182        }
    … …  
    129184        /**
    130185         * Enqueue our needed assets.
     186         *
     187         * @since 2.2.0
    131188         *
    132189         * @access public
    … …  
    145202
    146203                        // Provide a global object to our JS file containing our REST API endpoint, and API nonce
    147                         // Nonce must be 'wp_rest'
     204                        // Nonce must be 'wp_rest'.
    148205                        wp_localize_script(
    149206                                'kindmeta',
    … …  
    154211                                        'success_message' => __( 'Your URL has been successfully retrieved and parsed', 'indieweb-post-kinds' ),
    155212                                        'clear_message'   => __( 'Are you sure you want to clear post properties?', 'indieweb-post-kinds' ),
     213                                        'error_message'   => __( 'Error: Unable to Retrieve', 'indieweb-post-kinds' ),
     214                                        'invalid_url'     => __( 'Invalid URL', 'indieweb-post-kinds' ),
     215                                        'media_title'     => __( 'Attach', 'indieweb-post-kinds' ),
     216                                        'media_button'    => __( 'Use this media', 'indieweb-post-kinds' ),
    156217                                )
    157218                        );
    … …  
    163224        /**
    164225         * Utility function to concatenate a list of post kinds.
     226         *
     227         * @since 2.7.5
    165228         *
    166229         * @access public
    … …  
    180243         * Utility function to separate out a list of post kinds.
    181244         *
     245         * @since 2.7.5
     246         *
    182247         * @access public
    183248         *
    … …  
    194259        /**
    195260         * Function to render date/time field inputs.
     261         *
     262         * @since 2.5.0
    196263         *
    197264         * @access public
    … …  
    206273                $tz_seconds = get_option( 'gmt_offset' ) * 3600;
    207274                $offset     = tz_seconds_to_offset( $tz_seconds );
    208                 $time       = divide_datetime( $datetime );
     275                $time       = Kind_Time::divide_datetime( $datetime );
    209276                if ( ! is_array( $time ) ) {
    210277                        $time = array();
    … …  
    213280                        $offset = $time['offset'];
    214281                }
    215                 $string  = '<label class="half ' . $class . '" for="' . $prefix . '">' . $label . '<br/>';
    216                 $string .= '<input class="date" type="date" name="' . $prefix . '_date" id="' . $prefix . '_date" value="' . ( $time['date'] ?? '' ) . '"/>';
    217                 $string .= '<input class="time" type="time" name="' . $prefix . '_time" id="' . $prefix . '_time" step="1" value="' . ( $time['time'] ?? '' ) . '"/>';
     282                $prefix  = esc_attr( $prefix );
     283                $string  = '<label class="half ' . esc_attr( $class ) . '" for="' . $prefix . '">' . esc_html( $label ) . '<br/>';
     284                $string .= '<input class="date" type="date" name="' . $prefix . '_date" id="' . $prefix . '_date" value="' . esc_attr( $time['date'] ?? '' ) . '"/>';
     285                $string .= '<input class="time" type="time" name="' . $prefix . '_time" id="' . $prefix . '_time" step="1" value="' . esc_attr( $time['time'] ?? '' ) . '"/>';
    218286                $string .= self::select_offset( $prefix, $offset );
    219287                $string .= '</label>';
    … …  
    224292         * Function to render our timezone choices.
    225293         *
     294         * @since 2.5.0
     295         *
    226296         * @access public
    227297         *
    … …  
    231301         */
    232302        public static function select_offset( $prefix, $select ) {
    233                 $string  = '<select name="' . $prefix . '_offset" id="' . $prefix . '_offset">';
     303                $string  = '<select name="' . esc_attr( $prefix ) . '_offset" id="' . esc_attr( $prefix ) . '_offset">';
    234304                $string .= self::timezone_offset_choice( $select );
    235305                $string .= '</select>';
    … …  
    240310         * Function to render options for a chosen timezone select field.
    241311         *
     312         * @since 3.0.0
     313         *
    242314         * @access public
    243315         *
    … …  
    248320                $tzlist = get_gmt_offsets();
    249321                $string = '';
    250                 foreach ( $tzlist as $key => $value ) {
    251                         $string .= '<option value="' . $value . '"';
     322                foreach ( $tzlist as $value ) {
     323                        $string .= '<option value="' . esc_attr( $value ) . '"';
    252324                        if ( $select === $value ) {
    253325                                $string .= ' selected';
    254326                        }
    255                         $string .= '>GMT' . $value . '</option>';
     327                        $string .= '>GMT' . esc_html( $value ) . '</option>';
    256328                }
    257329                return $string;
    … …  
    261333         * Render the options for the rating select field.
    262334         *
    263          * @access public
    264          *
    265          * @param string $selected Selected Rating choice
     335         * @since 3.7.0
     336         *
     337         * @access public
     338         *
     339         * @param string $selected Selected Rating choice.
    266340         * @return string
    267341         */
    … …  
    278352                $ret      = '';
    279353                foreach ( $ratings as $key => $value ) {
    280                         $ret .= '<option value="' . $key . '"';
     354                        $ret .= '<option value="' . esc_attr( $key ) . '"';
    281355                        if ( $selected === $key ) {
    282356                                $ret .= ' selected';
    283357                        }
    284                         $ret .= '>' . $value . '</option>';
     358                        $ret .= '>' . esc_html( (string) $value ) . '</option>';
    285359                }
    286360                return $ret;
    … …  
    290364         * Render the options for the RSVP select field.
    291365         *
    292          * @access public
    293          *
    294          * @param string $selected Selected RSVP choice
     366         * @since 3.0.0
     367         *
     368         * @access public
     369         *
     370         * @param string $selected Selected RSVP choice.
    295371         * @return string
    296372         */
    … …  
    306382                $string = '';
    307383                foreach ( $rsvps as $key => $value ) {
    308                         $string .= '<option value="' . $key . '"';
     384                        $string .= '<option value="' . esc_attr( $key ) . '"';
    309385                        if ( $selected === $key ) {
    310386                                $string .= ' selected';
    311387                        }
    312                         $string .= '>' . $value . '</option>';
     388                        $string .= '>' . esc_html( (string) $value ) . '</option>';
    313389                }
    314390                return $string;
    … …  
    317393        /**
    318394         * Render our RSVP select input.
     395         *
     396         * @since 2.5.0
    319397         *
    320398         * @access public
    … …  
    324402         */
    325403        public static function rsvp_select( $selected ) {
    326                 $string  = '<label for="mf2_rsvp">' . __( 'RSVP', 'indieweb-post-kinds' ) . '</label><br/>';
     404                $string  = '<label for="mf2_rsvp">' . esc_html__( 'RSVP', 'indieweb-post-kinds' ) . '</label><br/>';
    327405                $string .= '<select name="mf2_rsvp" id="mf2_rsvp">';
    328406                $string .= self::rsvp_choice( $selected );
    … …  
    334412         * Render our rating select input.
    335413         *
     414         * @since 3.7.0
     415         *
    336416         * @access public
    337417         *
    … …  
    340420         */
    341421        public static function rating_select( $selected ) {
    342                 $string  = '<label for="mf2_rating">' . __( 'Rating', 'indieweb-post-kinds' ) . '</label><br/>';
     422                $string  = '<label for="mf2_rating">' . esc_html__( 'Rating', 'indieweb-post-kinds' ) . '</label><br/>';
    343423                $string .= '<select name="mf2_rating" id="mf2_rating">';
    344424                $string .= self::rating_choice( $selected );
    … …  
    348428
    349429        /**
     430         * Keeps the Response Properties box from being saved as hidden.
     431         *
     432         * The editor script hides the box for kinds without a response, such as
     433         * note. WordPress saves every hidden box as unticked in Screen Options
     434         * whenever any box is collapsed or any Screen Options box is toggled, so
     435         * the box would stay hidden for every kind afterwards. The script shows
     436         * and hides it by kind instead.
     437         *
     438         * @since 4.0.0
     439         *
     440         * @param string[]  $hidden IDs of the hidden meta boxes.
     441         * @param WP_Screen $screen The current screen.
     442         * @return string[] IDs of the hidden meta boxes.
     443         */
     444        public static function hidden_meta_boxes( $hidden, $screen ) {
     445                if ( ! is_array( $hidden ) || ! $screen instanceof WP_Screen || 'post' !== $screen->id ) {
     446                        return $hidden;
     447                }
     448                return array_values( array_diff( $hidden, array( 'replybox-meta' ) ) );
     449        }
     450
     451        /**
    350452         * Create one or more meta boxes to be displayed on the post editor screen.
     453         *
     454         * @since 2.2.0
    351455         *
    352456         * @access public
    … …  
    354458        public static function add_meta_boxes() {
    355459                add_meta_box(
    356                         'replybox-meta', // Unique ID
    357                         esc_html__( 'Response Properties', 'indieweb-post-kinds' ), // Title
    358                         array( static::class, 'reply_metabox' ), // Callback function
     460                        'replybox-meta', // Unique ID.
     461                        esc_html__( 'Response Properties', 'indieweb-post-kinds' ), // Title.
     462                        array( static::class, 'reply_metabox' ), // Callback function.
    359463                        'post',
    360                         'kind_after_title', // Context
    361                         'default', // Priority
     464                        'kind_after_title', // Context.
     465                        'default', // Priority.
    362466                        array(
    363467                                '__block_editor_compatible_meta_box' => false,
    … …  
    370474         * Render our reply meta box.
    371475         *
     476         * @since 3.0.0
     477         *
    372478         * @access public
    373479         *
    … …  
    382488         * Process and save meta box data.
    383489         *
    384          * @access public
    385          *
    386          * @param int    $post_id Saved post ID.
    387          * @param WP_Pos $post    Saved post object.
     490         * @since 2.2.0
     491         *
     492         * @access public
     493         *
     494         * @param int     $post_id Saved post ID.
     495         * @param WP_Post $post    Saved post object.
    388496         */
    389497        public static function save_post( $post_id, $post ) {
    … …  
    399507
    400508                // Verify that the nonce is valid.
    401                 if ( ! wp_verify_nonce( $_POST['replykind_metabox_nonce'], 'replykind_metabox' ) ) {
     509                if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['replykind_metabox_nonce'] ) ), 'replykind_metabox' ) ) {
    402510                        return;
    403511                }
    … …  
    409517
    410518                // Check the user's permissions.
    411                 if ( isset( $_POST['post_type'] ) && 'page' === $_POST['post_type'] ) {
     519                if ( 'page' === self::posted_text( 'post_type' ) ) {
    412520                        if ( ! current_user_can( 'edit_page', $post_id ) ) {
    413521                                return;
    … …  
    422530
    423531                if ( isset( $_POST['mf2_start_date'] ) || isset( $_POST['mf2_start_time'] ) ) {
    424                         $start = build_iso8601_time( sanitize_text_field( $_POST['mf2_start_date'] ), sanitize_text_field( $_POST['mf2_start_time'] ), sanitize_text_field( $_POST['mf2_start_offset'] ) );
     532                        $start = build_iso8601_time( self::posted_text( 'mf2_start_date' ), self::posted_text( 'mf2_start_time' ), self::posted_text( 'mf2_start_offset' ) );
    425533                        if ( ! $start ) {
    426534                                $kind_post->delete( 'start' );
    … …  
    430538                }
    431539                if ( isset( $_POST['mf2_end_date'] ) || isset( $_POST['mf2_end_time'] ) ) {
    432                         $end = build_iso8601_time( sanitize_text_field( $_POST['mf2_end_date'] ), sanitize_text_field( $_POST['mf2_end_time'] ), sanitize_text_field( $_POST['mf2_end_offset'] ) );
     540                        $end = build_iso8601_time( self::posted_text( 'mf2_end_date' ), self::posted_text( 'mf2_end_time' ), self::posted_text( 'mf2_end_offset' ) );
    433541                        if ( ! $end ) {
    434542                                $kind_post->delete( 'end' );
    … …  
    443551
    444552                $durations = array(
    445                         'Y' => intval( $_POST['duration_years'] ?? '' ),
    446                         'M' => intval( $_POST['duration_months'] ?? '' ),
    447                         'D' => intval( $_POST['duration_days'] ?? '' ),
    448                         'H' => intval( $_POST['duration_hours'] ?? '' ),
    449                         'I' => intval( $_POST['duration_minutes'] ?? '' ),
    450                         'S' => intval( $_POST['duration_seconds'] ?? '' ),
     553                        'Y' => absint( self::posted_text( 'duration_years' ) ),
     554                        'M' => absint( self::posted_text( 'duration_months' ) ),
     555                        'D' => absint( self::posted_text( 'duration_days' ) ),
     556                        'H' => absint( self::posted_text( 'duration_hours' ) ),
     557                        'I' => absint( self::posted_text( 'duration_minutes' ) ),
     558                        'S' => absint( self::posted_text( 'duration_seconds' ) ),
    451559                );
    452560                $duration  = build_iso8601_duration( $durations );
    … …  
    456564                        if ( $duration instanceof DateInterval ) {
    457565                                $duration = date_interval_to_iso8601( $duration );
    458                                 error_log( wp_json_encode( $duration ) );
    459566                        }
    460567                }
    … …  
    465572                }
    466573
    467                 $kind_post->set( 'rsvp', $_POST['mf2_rsvp'] );
    468                 if ( array_key_exists( 'mf2_rating', $_POST ) && is_numeric( $_POST['mf2_rating'] ) ) {
    469                         $kind_post->set( 'rating', intval( $_POST['mf2_rating'] ) );
    470                 }
    471 
    472                 if ( isset( $_POST['cite_published_date'] ) || isset( $_POST['published_time'] ) ) {
    473                         $cite['published'] = build_iso8601_time( sanitize_text_field( $_POST['cite_published_date'] ), sanitize_text_field( $_POST['cite_published_time'] ), sanitize_text_field( $_POST['cite_published_offset'] ) );
     574                if ( isset( $_POST['mf2_rsvp'] ) ) {
     575                        $rsvp = self::posted_text( 'mf2_rsvp' );
     576                        if ( in_array( $rsvp, array( 'yes', 'no', 'maybe', 'interested', 'remote' ), true ) ) {
     577                                $kind_post->set( 'rsvp', $rsvp );
     578                        } else {
     579                                $kind_post->delete( 'rsvp' );
     580                        }
     581                }
     582                $rating = self::posted_text( 'mf2_rating' );
     583                if ( is_numeric( $rating ) ) {
     584                        $kind_post->set( 'rating', intval( $rating ) );
     585                }
     586
     587                if ( isset( $_POST['cite_published_date'] ) || isset( $_POST['cite_published_time'] ) ) {
     588                        $cite['published'] = build_iso8601_time( self::posted_text( 'cite_published_date' ), self::posted_text( 'cite_published_time' ), self::posted_text( 'cite_published_offset' ) );
    474589                }
    475590                if ( isset( $_POST['cite_updated_date'] ) || isset( $_POST['cite_updated_time'] ) ) {
    476                         $cite['updated'] = build_iso8601_time( sanitize_text_field( $_POST['cite_updated_date'] ), sanitize_text_field( $_POST['cite_updated_time'] ), sanitize_text_field( $_POST['cite_updated_offset'] ) );
    477                 }
    478                 $cite['summary'] = wp_kses_post( $_POST['cite_summary'] ?? '' );
    479                 $cite['name']    = sanitize_text_field( $_POST['cite_name'] ?? '' );
    480                 $cite['url']     = esc_url( $_POST['cite_url'] ?? '' );
     591                        $cite['updated'] = build_iso8601_time( self::posted_text( 'cite_updated_date' ), self::posted_text( 'cite_updated_time' ), self::posted_text( 'cite_updated_offset' ) );
     592                }
     593                // Summaries are plain text; line breaks are kept.
     594                $cite['summary'] = isset( $_POST['cite_summary'] ) && is_string( $_POST['cite_summary'] ) ? sanitize_textarea_field( wp_unslash( $_POST['cite_summary'] ) ) : '';
     595                $cite['name']    = self::posted_text( 'cite_name' );
     596                $cite['url']     = self::posted_urls( 'cite_url' );
    481597                if ( isset( $_POST['cite_tags'] ) ) {
    482                         $cite['category'] = array_filter( explode( ';', sanitize_text_field( $_POST['cite_tags'] ) ) );
    483                 }
    484                 $cite['publication'] = sanitize_text_field( $_POST['cite_publication'] ?? '' );
    485                 $cite['featured']    = esc_url( $_POST['cite_featured'] ?? '' );
    486 
    487                 $author         = array();
    488                 $author['name'] = self::explode( sanitize_text_field( $_POST['cite_author_name'] ?? '' ) );
    489                 $author['url']  = self::explode( $_POST['cite_author_url'] ?? '' );
    490                 if ( is_array( $author['url'] ) ) {
    491                         $author['url'] = array_map( 'esc_url', $author['url'] );
     598                        $cite['category'] = array_values( array_filter( array_map( 'trim', explode( ';', self::posted_text( 'cite_tags' ) ) ) ) );
     599                }
     600                $cite['publication'] = self::posted_text( 'cite_publication' );
     601                $cite['featured']    = self::posted_urls( 'cite_featured' );
     602
     603                // One h-card per author. Like the other fields, an empty value clears what was stored.
     604                $authors = array_map( '\ParseThis\jf2_to_mf2', self::posted_authors() );
     605                if ( empty( $authors ) ) {
     606                        $cite['author'] = '';
    492607                } else {
    493                         $author['url'] = esc_url( $author['url'] );
    494                 }
    495 
    496                 $author['photo'] = self::explode( $_POST['cite_author_photo'] ?? '' );
    497 
    498                 if ( is_array( $author['photo'] ) ) {
    499                         $author['photo'] = array_map( 'esc_url', $author['photo'] );
    500                 } else {
    501                         $author['photo'] = esc_url( $author['photo'] );
    502                 }
    503 
    504                 $author = array_filter( $author );
    505                 if ( ! empty( $author ) ) {
    506 
    507                         $author['type'] = 'card';
    508                         $cite['author'] = jf2_to_mf2( $author );
    509                 }
     608                        $cite['author'] = 1 === count( $authors ) ? $authors[0] : $authors;
     609                }
     610
    510611                $kind = $kind_post->get_kind();
    511612                $type = Kind_Taxonomy::get_kind_info( $kind, 'property' );
    512                 // Make sure there is no overwrite of properties that might not be handled by the plugin
    513                 $fetch = $kind_post->get_cite();
    514                 if ( ! $fetch ) {
    515                         $fetch = array();
    516                 } else {
    517                         $fetch = array_filter( $fetch );
    518                 }
     613                // Make sure there is no overwrite of properties that might not be handled by the plugin.
     614                $fetch = self::stored_cite_properties( $kind_post->get_cite() );
    519615                if ( empty( $_POST['cite_media'] ) ) {
    520616                        $cite = array_merge( $fetch, $cite );
    … …  
    531627                                }
    532628                                $cite = array( 'properties' => $build );
    533                                 // Temporary code which assumes everything except a checkin is a citation
     629                                // Temporary code which assumes everything except a checkin is a citation.
    534630                                if ( 'checkin' === $kind ) {
    535631                                        $cite['type'] = array( 'h-card' );
    … …  
    541637                        }
    542638                }
    543                 $kind_post->set( $type, $cite );
     639                // Values are unslashed for sanitizing; the metadata API expects them slashed.
     640                $kind_post->set( $type, wp_slash( $cite ) );
     641        }
     642
     643        /**
     644         * Returns a posted metabox field as sanitized text.
     645         *
     646         * Only called from save_post(), after the nonce is verified.
     647         *
     648         * @since 4.0.0
     649         *
     650         * @param string $key Field name.
     651         * @return string The unslashed, sanitized value, or an empty string if it is missing.
     652         */
     653        private static function posted_text( $key ) {
     654                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in save_post().
     655                if ( ! isset( $_POST[ $key ] ) || ! is_string( $_POST[ $key ] ) ) {
     656                        return '';
     657                }
     658                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in save_post().
     659                return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
     660        }
     661
     662        /**
     663         * Returns the properties of a stored citation, keyed by property name.
     664         *
     665         * Saving merges these with the posted fields, so properties the metabox
     666         * does not edit are kept. A citation stored as a plain URL has none.
     667         * Before 4.0.0, saving merged the whole stored microformat instead, nesting
     668         * it inside its own properties one level deeper on each save; those levels
     669         * are flattened here, newer values winning.
     670         *
     671         * @since 4.0.0
     672         *
     673         * @param mixed $cite Citation as returned by Kind_Post::get_cite().
     674         * @return array Property values, each a list.
     675         */
     676        private static function stored_cite_properties( $cite ) {
     677                if ( wp_is_numeric_array( $cite ) && 1 === count( $cite ) ) {
     678                        $cite = $cite[0];
     679                }
     680                if ( ! is_array( $cite ) || empty( $cite['properties'] ) || ! is_array( $cite['properties'] ) ) {
     681                        return array();
     682                }
     683                return array_filter( Kind_Upgrade::flatten_properties( $cite['properties'] ) );
     684        }
     685
     686        /**
     687         * Returns a posted metabox URL field, sanitized.
     688         *
     689         * Only called from save_post(), after the nonce is verified.
     690         *
     691         * @since 4.0.0
     692         *
     693         * @param string $key Field name.
     694         * @return string The URL, or an empty string if it is not valid.
     695         */
     696        private static function posted_urls( $key ) {
     697                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in save_post().
     698                if ( ! isset( $_POST[ $key ] ) || ! is_string( $_POST[ $key ] ) ) {
     699                        return '';
     700                }
     701                // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified in save_post(); sanitized with sanitize_url() (sanitize_text_field() would alter %-encoding).
     702                return sanitize_url( trim( wp_unslash( $_POST[ $key ] ) ) );
     703        }
     704
     705        /**
     706         * Returns the authors posted in the metabox's Author tab, as jf2 h-cards.
     707         *
     708         * Each field holds one value per author, separated by semicolons, so the
     709         * first name goes with the first URL and the first photo, and so on. An
     710         * author without a URL leaves that position empty, as in "Ann; Bob" and
     711         * "; https://example.com/bob".
     712         *
     713         * Only called from save_post(), after the nonce is verified.
     714         *
     715         * @since 4.0.0
     716         *
     717         * @return array[] The authors, each with type 'card' and any of name, url and photo.
     718         */
     719        private static function posted_authors() {
     720                $fields = array();
     721                foreach ( array(
     722                        'name'  => 'cite_author_name',
     723                        'url'   => 'cite_author_url',
     724                        'photo' => 'cite_author_photo',
     725                ) as $property => $key ) {
     726                        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified in save_post(); each value is sanitized below.
     727                        $value  = isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : '';
     728                        $values = array_map( 'trim', explode( ';', $value ) );
     729
     730                        $fields[ $property ] = array_map( 'name' === $property ? 'sanitize_text_field' : 'sanitize_url', $values );
     731                }
     732
     733                $authors = array();
     734                $count   = max( array_map( 'count', $fields ) );
     735                for ( $i = 0; $i < $count; $i++ ) {
     736                        $author = array();
     737                        foreach ( $fields as $property => $values ) {
     738                                if ( isset( $values[ $i ] ) && '' !== $values[ $i ] ) {
     739                                        $author[ $property ] = $values[ $i ];
     740                                }
     741                        }
     742                        if ( $author ) {
     743                                $authors[] = array_merge( array( 'type' => 'card' ), $author );
     744                        }
     745                }
     746                return $authors;
    544747        }
    545748
    546749        /**
    547750         * Function to handle saving our kind data upon post status transition.
     751         *
     752         * @since 2.2.0
    548753         *
    549754         * @access public
  • indieweb-post-kinds/trunk/includes/class-kind-plugins.php

    r2973430 r3738687  
    11<?php
    2 
    32/**
    4  * Post Kind Plugins Class
     3 * Kind_Plugins class.
    54 *
    6  * Custom Functions for Specific Other Pugins
     5 * @package Post_Kinds
     6 * @since   2.5.2
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Integrations with other plugins, such as Micropub and ActivityPub.
    713 *
    8  * @package Post Kinds
     14 * @since 2.5.2
    915 */
    1016class Kind_Plugins {
    1117
    1218        /**
     19         * WP-Cron hook that retries parsing a Micropub citation in the background.
     20         *
     21         * @var string
     22         */
     23        const ENRICH_EVENT = 'post_kinds_enrich_citation';
     24
     25        /**
     26         * How many times a citation is parsed after temporary failures, counting the
     27         * attempt during the request.
     28         *
     29         * @var int
     30         */
     31        const ENRICH_ATTEMPTS = 3;
     32
     33        /**
     34         * Post meta holding the log of citation parsing outcomes for a post.
     35         *
     36         * @var string
     37         */
     38        const LOG_META = '_kind_citation_log';
     39
     40        /**
     41         * Post meta holding the post's citation status: retrying or failed. It is
     42         * removed when no citation is retrying or failed.
     43         *
     44         * @var string
     45         */
     46        const STATUS_META = '_kind_citation_status';
     47
     48        /**
     49         * How many log entries are kept per post.
     50         *
     51         * @var int
     52         */
     53        const LOG_LIMIT = 20;
     54
     55        /**
     56         * Citation URLs from the current Micropub request that could not be parsed,
     57         * as arrays of property, URL and WP_Error, waiting for the post ID.
     58         *
     59         * @var array[]
     60         */
     61        private static $pending = array();
     62
     63        /**
    1364         * Initialize our plugin integrations.
    1465         *
     66         * @since 2.5.2
     67         *
    1568         * @access public
    1669         */
    1770        public static function init() {
    18                 // Set Post Kind for Micropub Inputs.
     71                // Set the post kind for Micropub posts before they are inserted (Micropub 2.1.0
     72                // and later), with after_micropub as a fallback.
     73                add_filter( 'micropub_tax_input', array( static::class, 'micropub_tax_input' ), 10, 2 );
    1974                add_action( 'after_micropub', array( static::class, 'micropub_set_kind' ), 9, 2 );
    2075                add_action( 'after_micropub', array( static::class, 'post_formats' ), 11, 2 );
    2176                add_filter( 'before_micropub', array( static::class, 'micropub_parse' ), 11 );
     77                add_action( 'after_micropub', array( static::class, 'schedule_enrichment' ), 20, 2 );
     78                add_action( self::ENRICH_EVENT, array( static::class, 'enrich_citation' ), 10, 4 );
     79                // Advertise the enabled kinds to Micropub clients (Micropub 1.4 and later).
     80                add_filter( 'micropub_query', array( static::class, 'micropub_query' ), 10, 2 );
    2281                add_filter( 'tempus_widget_post_title', array( static::class, 'tempus_widget_post_title' ), 10, 2 );
    2382                // Override Post Type in Semantic Linkbacks.
    2483                add_filter( 'semantic_linkbacks_post_type', array( static::class, 'semantic_post_type' ), 11, 2 );
    2584
    26                 // Remove the Automatic Post Generation that the Micropub Plugin Offers
    27                 if ( class_exists( 'Micropub_Render' ) ) {
    28                         if ( has_filter( 'micropub_post_content', array( 'Micropub_Render', 'generate_post_content' ) ) ) {
    29                                 remove_filter( 'micropub_post_content', array( 'Micropub_Render', 'generate_post_content' ), 1, 2 );
    30                         }
    31                 } elseif ( class_exists( 'Micropub_Plugin' ) ) {
    32                         if ( has_filter( 'micropub_post_content', array( 'Micropub_Plugin', 'generate_post_content' ) ) ) {
    33                                 remove_filter( 'micropub_post_content', array( 'Micropub_Plugin', 'generate_post_content' ), 1, 2 );
    34                         }
    35                 }
    36 
    37                 // Hum Compatibility Filters
     85                // Post Kinds displays the response itself, so turn off Micropub's dynamic
     86                // rendering of Micropub posts (Micropub 2.4.0 and later).
     87                add_filter( 'micropub_dynamic_render', '__return_false' );
     88
     89                // Hum Compatibility Filters.
    3890                add_action( 'hum_local_types', array( static::class, 'hum_local_types' ), 11 );
    3991                add_action( 'hum_type_prefix', array( static::class, 'hum_type_prefix' ), 11, 2 );
    4092
    41                 // ActivityPub Filters
     93                // ActivityPub Filters.
    4294                add_filter( 'activitypub_post', array( static::class, 'activitypub_post' ), 12 );
    4395        }
    … …  
    4799         * Construct a title for the Tempus Widget link.
    48100         *
     101         * @since 3.5.4
     102         *
    49103         * @access public
    50104         *
    51          * @param string $title The Original Title.
    52          * @param WP_Post $post Post object.
     105         * @param string  $title The Original Title.
     106         * @param WP_Post $post  Post object.
    53107         * @return string
    54108         */
    … …  
    58112
    59113        /**
    60          *
    61          * @param array $post_array
     114         * Sets the ActivityPub object type based on the post kind.
     115         *
     116         * @since 3.6.2
     117         *
     118         * @param array $post_array ActivityPub object array.
    62119         *
    63120         * @return array
    … …  
    93150        }
    94151
     152        /**
     153         * Adds the kinds' shortlink type codes to the Hum plugin.
     154         *
     155         * @since 3.4.0
     156         *
     157         * @param string[] $types Local type codes.
     158         * @return string[] Local type codes.
     159         */
    95160        public static function hum_local_types( $types ) {
    96161                // http://tantek.pbworks.com/w/page/21743973/Whistle#design - Some of the uses are modified based on design considerations noted.
    97                 $types[] = 'f'; // Favorited, Likes, etc
    98                 $types[] = 'e'; // Events
    99                 $types[] = 'g'; // Geo Checkin
    100                 $types[] = 'h'; // Link
    101                 $types[] = 'm'; // Metric
    102                 $types[] = 'q'; // Question
    103                 $types[] = 'r'; // Review
    104                 $types[] = 'x'; // Experience
    105                 $types[] = 'u'; // Status Update
     162                $types[] = 'f'; // Favorited, Likes, etc.
     163                $types[] = 'e'; // Events.
     164                $types[] = 'g'; // Geo Checkin.
     165                $types[] = 'h'; // Link.
     166                $types[] = 'm'; // Metric.
     167                $types[] = 'q'; // Question.
     168                $types[] = 'r'; // Review.
     169                $types[] = 'x'; // Experience.
     170                $types[] = 'u'; // Status Update.
    106171                return $types;
    107172        }
    108173
     174        /**
     175         * Uses the kind's shortlink code as a post's Hum type prefix.
     176         *
     177         * @since 3.4.0
     178         *
     179         * @param string $prefix  Type prefix.
     180         * @param int    $post_id Post ID.
     181         * @return string The kind's shortlink code, or the prefix if it has none.
     182         */
    109183        public static function hum_type_prefix( $prefix, $post_id ) {
    110184                $post_type = get_post_type( $post_id );
    … …  
    124198         * Replaces need for replacing the entire excerpt.
    125199         *
     200         * @since 2.5.2
     201         *
    126202         * @access public
    127203         *
    … …  
    135211
    136212        /**
    137          * Take mf2 properties and set a post kind.
    138          * Implements Post Type Discovery https://www.w3.org/TR/post-type-discovery/
    139          *
    140          * @param array $input   Micropub Request in JSON.
    141          * @param array $wp_args Arguments passed to insert or update posts.
     213         * Returns the post kind for an mf2 object, using Post Type Discovery.
     214         * Implements https://www.w3.org/TR/post-type-discovery/ via Parse This.
     215         *
     216         * @since 4.0.0
     217         *
     218         * @param array $mf2 mf2 object, such as a Micropub request.
     219         * @return string Registered kind slug, or an empty string if none applies.
     220         */
     221        public static function discover_kind( $mf2 ) {
     222                if ( ! is_array( $mf2 ) || empty( $mf2['properties'] ) || ! function_exists( '\\ParseThis\\post_type_discovery' ) ) {
     223                        return '';
     224                }
     225                $type = \ParseThis\post_type_discovery( \ParseThis\mf2_to_jf2( $mf2 ) );
     226                return ( $type && Kind_Taxonomy::get_post_kind_info( $type ) ) ? $type : '';
     227        }
     228
     229        /**
     230         * Sets the kind of a Micropub post in the taxonomy input, so the post is
     231         * inserted with its kind rather than the default term. Everything that runs
     232         * during the insert, such as the publish transition, post formats and other
     233         * plugins' publish hooks, then sees the right kind.
     234         *
     235         * Hooked to Micropub's micropub_tax_input filter (Micropub 2.1.0 and later),
     236         * which is applied when a post is created.
     237         *
     238         * @since 4.0.0
     239         *
     240         * @param array|null $tax_input Taxonomy input for the new post.
     241         * @param array      $input     Micropub request.
     242         * @return array|null Taxonomy input, with the kind added.
     243         */
     244        public static function micropub_tax_input( $tax_input, $input ) {
     245                $kind = self::discover_kind( $input );
     246                if ( ! $kind ) {
     247                        return $tax_input;
     248                }
     249                if ( ! is_array( $tax_input ) ) {
     250                        $tax_input = array();
     251                }
     252                if ( empty( $tax_input['kind'] ) ) {
     253                        $tax_input['kind'] = array( $kind );
     254                }
     255                return $tax_input;
     256        }
     257
     258        /**
     259         * Sets the kind of a Micropub post after it is created, if it does not
     260         * already have it.
     261         *
     262         * The kind is normally set before the insert (see micropub_tax_input()). This
     263         * covers Micropub versions before 2.1.0, and users who cannot assign kind
     264         * terms, for whom WordPress ignores the taxonomy input.
     265         *
     266         * @since 2.5.2
     267         *
     268         * @param array      $input   Micropub request.
     269         * @param array|null $wp_args Arguments of the created or updated post; null for queries.
    142270         */
    143271        public static function micropub_set_kind( $input, $wp_args ) {
    144                 // Only continue if create or update
    145                 if ( ! $wp_args ) {
     272                if ( empty( $wp_args['ID'] ) ) {
    146273                        return;
    147274                }
    148                 $type = post_type_discovery( mf2_to_jf2( $input ) );
    149                 if ( ! empty( $type ) ) {
    150                         set_post_kind( $wp_args['ID'], $type );
    151                 }
    152         }
    153 
    154         /**
    155          * Set our post formats.
     275                $post_id = (int) $wp_args['ID'];
     276                if ( isset( $input['action'] ) && 'update' === $input['action'] ) {
     277                        // Only recalculate when the update changed a property that decides the
     278                        // kind, so a kind chosen in the editor survives unrelated updates.
     279                        if ( ! self::update_changes_kind( $input ) ) {
     280                                return;
     281                        }
     282                        $kind = self::discover_kind( self::stored_mf2( $post_id ) );
     283                } else {
     284                        $kind = self::discover_kind( $input );
     285                }
     286                if ( $kind && get_post_kind_slug( $post_id ) !== $kind ) {
     287                        self::set_kind_without_moving_properties( $post_id, $kind );
     288                }
     289        }
     290
     291        /**
     292         * Returns the properties that decide a post's kind: each registered kind's
     293         * property, plus rsvp, which Post Type Discovery checks first.
     294         *
     295         * @since 4.0.0
     296         *
     297         * @return string[] Property names.
     298         */
     299        public static function kind_properties() {
     300                $properties = array( 'rsvp' );
     301                foreach ( Kind_Taxonomy::get_kind_list() as $kind ) {
     302                        $property = Kind_Taxonomy::get_kind_info( $kind, 'property' );
     303                        if ( $property ) {
     304                                $properties[] = $property;
     305                        }
     306                }
     307                return array_values( array_unique( $properties ) );
     308        }
     309
     310        /**
     311         * Whether a Micropub update request replaces, adds or deletes a property
     312         * that decides the kind.
     313         *
     314         * @since 4.0.0
     315         *
     316         * @param array $input Micropub update request.
     317         * @return bool
     318         */
     319        public static function update_changes_kind( $input ) {
     320                $changed = array();
     321                foreach ( array( 'replace', 'add' ) as $operation ) {
     322                        if ( isset( $input[ $operation ] ) && is_array( $input[ $operation ] ) ) {
     323                                $changed = array_merge( $changed, array_keys( $input[ $operation ] ) );
     324                        }
     325                }
     326                if ( isset( $input['delete'] ) && is_array( $input['delete'] ) ) {
     327                        // Either a list of property names, or an object of property => values.
     328                        $changed = array_merge( $changed, wp_is_numeric_array( $input['delete'] ) ? $input['delete'] : array_keys( $input['delete'] ) );
     329                }
     330                return (bool) array_intersect( $changed, self::kind_properties() );
     331        }
     332
     333        /**
     334         * Builds the mf2 for a post from what is stored: the mf2_ post meta, plus the
     335         * title and content, which Post Type Discovery uses to tell articles from notes.
     336         *
     337         * @since 4.0.0
     338         *
     339         * @param int $post_id Post ID.
     340         * @return array mf2 object.
     341         */
     342        public static function stored_mf2( $post_id ) {
     343                $properties = array();
     344                foreach ( get_post_meta( $post_id ) as $key => $values ) {
     345                        if ( 0 !== strpos( $key, 'mf2_' ) || 'mf2_type' === $key ) {
     346                                continue;
     347                        }
     348                        $value = maybe_unserialize( $values[0] );
     349                        if ( '' === $value || array() === $value ) {
     350                                continue;
     351                        }
     352                        $properties[ substr( $key, 4 ) ] = is_array( $value ) ? $value : array( $value );
     353                }
     354                $post = get_post( $post_id );
     355                if ( $post && '' !== $post->post_title ) {
     356                        $properties['name'] = array( $post->post_title );
     357                }
     358                if ( $post && '' !== $post->post_content ) {
     359                        $properties['content'] = array( $post->post_content );
     360                }
     361                return array(
     362                        'type'       => array( 'h-entry' ),
     363                        'properties' => $properties,
     364                );
     365        }
     366
     367        /**
     368         * Sets a post's kind without moving property values between properties.
     369         *
     370         * When the kind changes in the editor, Kind_Metabox::change_kind() moves the
     371         * old kind's property to the new one (a like-of becomes an in-reply-to, for
     372         * example). After a Micropub update the properties are already where the
     373         * client put them, so that is suspended here.
     374         *
     375         * @since 4.0.0
     376         *
     377         * @param int    $post_id Post ID.
     378         * @param string $kind    Kind slug.
     379         */
     380        private static function set_kind_without_moving_properties( $post_id, $kind ) {
     381                $moving = has_action( 'change_kind', array( 'Kind_Metabox', 'change_kind' ) );
     382                if ( false !== $moving ) {
     383                        remove_action( 'change_kind', array( 'Kind_Metabox', 'change_kind' ), $moving );
     384                }
     385                set_post_kind( $post_id, $kind );
     386                if ( false !== $moving ) {
     387                        add_action( 'change_kind', array( 'Kind_Metabox', 'change_kind' ), $moving, 3 );
     388                }
     389        }
     390
     391        /**
     392         * Sets the post format of a post created or updated by Micropub to match its kind.
     393         *
     394         * @since 2.5.2
    156395         *
    157396         * @access public
    158397         *
    159          * @param $input
    160          * @param $wp_args
     398         * @param array $input   The Micropub request.
     399         * @param array $wp_args Arguments passed to wp_insert_post() or wp_update_post().
    161400         */
    162401        public static function post_formats( $input, $wp_args ) {
    … …  
    169408
    170409        /**
    171          * Parse our micropub values.
     410         * Returns the Micropub properties whose URL values are enriched into citations.
     411         *
     412         * @since 4.0.0
     413         *
     414         * @return string[] Property names.
     415         */
     416        public static function citation_properties() {
     417                return array( 'bookmark-of', 'like-of', 'favorite-of', 'in-reply-to', 'read-of', 'listen-of', 'watch-of' );
     418        }
     419
     420        /**
     421         * Enriches the URLs in citation properties of a Micropub create request, or
     422         * of the replace part of an update request, into h-cite objects parsed from
     423         * the cited page.
     424         *
     425         * Follows the Micropub spec: property values are arrays, so a property whose
     426         * value is not an array is left for Micropub to handle, and only string URLs
     427         * inside the array are enriched. Values the client already sent as objects,
     428         * such as an h-cite, are kept as sent.
     429         *
     430         * @since 2.7.5
    172431         *
    173432         * @access public
    174433         *
    175          * @param array $input Array of inputs to parse.
    176          * @return mixed
     434         * @param array $input Micropub request, in mf2 JSON.
     435         * @return array The request, with citation URLs enriched where possible.
    177436         */
    178437        public static function micropub_parse( $input ) {
    179                 if ( ! $input ) {
     438                self::$pending = array();
     439                // Queries (q) are left as is.
     440                if ( ! is_array( $input ) || isset( $input['q'] ) ) {
    180441                        return $input;
    181442                }
    182                 // q indicates a get query
    183                 if ( isset( $input['q'] ) ) {
     443                if ( ! class_exists( '\\ParseThis\\Parser' ) || ! self::enrichment_enabled( $input ) ) {
    184444                        return $input;
    185445                }
    186                 if ( ! isset( $input['properties'] ) ) {
    187                         return $input;
    188                 }
    189                 $parsed = array( 'bookmark-of', 'like-of', 'favorite-of', 'in-reply-to', 'read-of', 'listen-of', 'watch-of' );
    190                 foreach ( $input['properties'] as $property => $value ) {
    191                         if ( in_array( $property, $parsed, true ) ) {
    192                                 if ( wp_is_numeric_array( $value ) ) {
    193                                         foreach ( $value as $i => $v ) {
    194                                                 if ( wp_http_validate_url( $v ) ) {
    195                                                         $parse = new Parse_This( $v );
    196                                                         $fetch = $parse->fetch();
    197                                                         if ( ! is_wp_error( $fetch ) ) {
    198                                                                 $parse->parse();
    199                                                                 $jf2 = $parse->get();
    200                                                                 // Entries become citations
    201                                                                 if ( 'entry' === $jf2['type'] ) {
    202                                                                         $jf2['type'] = 'cite';
    203                                                                 }
    204                                                                 $mf2                                    = jf2_to_mf2( $jf2 );
    205                                                                 $input['properties'][ $property ][ $i ] = $mf2;
    206                                                         } else {
    207                                                                 error_log( wp_json_encode( $fetch ) ); // phpcs:ignore
    208                                                         }
    209                                                 }
    210                                         }
    211                                 } elseif ( isset( $value['url'] ) && wp_http_validate_url( $value['url'] ) ) {
    212                                         $parse = new Parse_This( $value['url'] );
    213                                         $fetch = $parse - fetch();
    214                                         if ( ! is_wp_error( $fetch ) ) {
    215                                                 $parse->parse();
    216                                                 $input['properties'][ $property ] = array_merge( $value, jf2_to_mf2( $parse->get() ) );
     446                // Create requests carry properties. Update requests can replace citation
     447                // properties; Micropub only allows adding categories, syndication and media.
     448                if ( isset( $input['properties'] ) && is_array( $input['properties'] ) ) {
     449                        $input['properties'] = self::enrich_properties( $input['properties'] );
     450                }
     451                if ( isset( $input['action'] ) && 'update' === $input['action'] && isset( $input['replace'] ) && is_array( $input['replace'] ) ) {
     452                        $input['replace'] = self::enrich_properties( $input['replace'] );
     453                }
     454                return $input;
     455        }
     456
     457        /**
     458         * Enriches the citation URLs in a set of Micropub properties.
     459         *
     460         * @since 4.0.0
     461         *
     462         * @param array $properties Properties, keyed by name, each an array of values.
     463         * @return array The properties, with citation URLs enriched where possible.
     464         */
     465        private static function enrich_properties( $properties ) {
     466                foreach ( self::citation_properties() as $property ) {
     467                        if ( empty( $properties[ $property ] ) || ! wp_is_numeric_array( $properties[ $property ] ) ) {
     468                                continue;
     469                        }
     470                        foreach ( $properties[ $property ] as $i => $value ) {
     471                                if ( ! is_string( $value ) || ! wp_http_validate_url( $value ) ) {
     472                                        continue;
     473                                }
     474                                // Keep the client waiting only briefly: a short timeout and no follow-up
     475                                // requests. Failures are logged, and retried in the background where
     476                                // worthwhile, once the post exists (see schedule_enrichment()).
     477                                $cite = self::parse_citation( $value, self::request_parse_args() );
     478                                if ( is_wp_error( $cite ) ) {
     479                                        self::report_failure( $value, $cite );
     480                                        self::$pending[] = array( $property, $value, $cite );
     481                                        continue;
     482                                }
     483                                $properties[ $property ][ $i ] = $cite;
     484                        }
     485                }
     486                return $properties;
     487        }
     488
     489        /**
     490         * Fetches and parses a URL into an mf2 citation.
     491         *
     492         * @since 4.0.0
     493         *
     494         * @access public
     495         *
     496         * @param string $url  URL of the cited page.
     497         * @param array  $args {
     498         *     Optional. Limits for this parse.
     499         *
     500         *     @type int $timeout      HTTP timeout in seconds. Default: Parse This's own (15).
     501         *     @type int $max_requests Follow-up requests allowed. Default: Parse This's own (10).
     502         * }
     503         * @return array|WP_Error mf2 citation, or WP_Error if the page could not be fetched or parsed.
     504         */
     505        public static function parse_citation( $url, $args = array() ) {
     506                $timeout       = isset( $args['timeout'] ) ? (int) $args['timeout'] : 0;
     507                $max_requests  = isset( $args['max_requests'] ) ? (int) $args['max_requests'] : null;
     508                $limit_timeout = static function ( $request ) use ( $timeout ) {
     509                        $request['timeout'] = isset( $request['timeout'] ) ? min( (float) $request['timeout'], $timeout ) : $timeout;
     510                        return $request;
     511                };
     512                $limit_budget  = static function () use ( $max_requests ) {
     513                        return $max_requests;
     514                };
     515                if ( $timeout > 0 ) {
     516                        add_filter( 'http_request_args', $limit_timeout, 99 );
     517                }
     518                if ( null !== $max_requests ) {
     519                        add_filter( 'parse_this_max_requests', $limit_budget, 99 );
     520                }
     521                try {
     522                        $parse = new \ParseThis\Parser( $url );
     523                        $fetch = $parse->fetch();
     524                        if ( is_wp_error( $fetch ) ) {
     525                                return $fetch;
     526                        }
     527                        // Keep nested objects in place rather than moving them to refs.
     528                        $parse->parse( array( 'references' => false ) );
     529                        $jf2 = $parse->get();
     530                } finally {
     531                        remove_filter( 'http_request_args', $limit_timeout, 99 );
     532                        remove_filter( 'parse_this_max_requests', $limit_budget, 99 );
     533                }
     534                if ( ! is_array( $jf2 ) || empty( $jf2 ) ) {
     535                        return new WP_Error( 'empty', __( 'Nothing could be parsed from the URL.', 'indieweb-post-kinds' ) );
     536                }
     537                // Content Parse This did not recognize is returned whole under raw.
     538                unset( $jf2['raw'] );
     539                $type = isset( $jf2['type'] ) ? $jf2['type'] : '';
     540                // A page that parses as a feed (a home page, for example) is cited as the
     541                // page itself, not its entries.
     542                if ( 'feed' === $type ) {
     543                        $jf2 = array_intersect_key( $jf2, array_flip( array( 'name', 'url', 'author', 'summary', 'photo' ) ) );
     544                }
     545                $jf2 = self::clean_citation( $jf2 );
     546                // Entries and feeds become citations. Other types, such as an h-event for
     547                // an RSVP, are kept.
     548                $jf2['type'] = in_array( $type, array( '', 'entry', 'feed' ), true ) ? 'cite' : $type;
     549                return \ParseThis\jf2_to_mf2( $jf2 );
     550        }
     551
     552        /**
     553         * Whether to enrich citation URLs in Micropub requests.
     554         *
     555         * @since 4.0.0
     556         *
     557         * @param array|null $input Micropub request, or null outside a request.
     558         * @return bool
     559         */
     560        public static function enrichment_enabled( $input = null ) {
     561                /**
     562                 * Filters whether Post Kinds enriches citation URLs in Micropub requests
     563                 * (in-reply-to, like-of, and so on) with data parsed from the cited page.
     564                 *
     565                 * Return false to store citations exactly as the client sent them.
     566                 *
     567                 * @since 4.0.0
     568                 *
     569                 * @param bool       $enabled Whether to enrich citations. Default true.
     570                 * @param array|null $input   Micropub request, or null when a background retry runs.
     571                 */
     572                return (bool) apply_filters( 'post_kinds_micropub_enrich', true, $input );
     573        }
     574
     575        /**
     576         * Returns the limits for parsing citations during a Micropub request.
     577         *
     578         * @since 4.0.0
     579         *
     580         * @return array Arguments for parse_citation().
     581         */
     582        public static function request_parse_args() {
     583                /**
     584                 * Filters the HTTP timeout, in seconds, for parsing a citation while a
     585                 * Micropub client waits. Citations that time out are retried in the
     586                 * background.
     587                 *
     588                 * @since 4.0.0
     589                 *
     590                 * @param int $timeout Timeout in seconds. Default 5.
     591                 */
     592                $timeout = (int) apply_filters( 'post_kinds_micropub_parse_timeout', 5 );
     593                return array(
     594                        'timeout'      => max( 1, $timeout ),
     595                        'max_requests' => 0,
     596                );
     597        }
     598
     599        /**
     600         * Classifies a parse failure to decide whether to retry it.
     601         *
     602         * @since 4.0.0
     603         *
     604         * @param WP_Error $error Parse failure.
     605         * @return string One of:
     606         *                'temporary'   A connection problem or timeout, a server error or rate limiting.
     607         *                'unavailable' The page was missing or refused (HTTP 404, 401 or 403), which
     608         *                              may be an outage or a configuration problem on the cited site.
     609         *                'permanent'   Anything else, such as content that cannot be parsed.
     610         */
     611        public static function failure_type( $error ) {
     612                if ( 'http_request_failed' === $error->get_error_code() ) {
     613                        return 'temporary';
     614                }
     615                $code = self::failure_response_code( $error );
     616                if ( 429 === $code || $code >= 500 ) {
     617                        return 'temporary';
     618                }
     619                if ( in_array( $code, array( 401, 403, 404 ), true ) ) {
     620                        return 'unavailable';
     621                }
     622                return 'permanent';
     623        }
     624
     625        /**
     626         * Returns the HTTP status code of a parse failure, if there was one.
     627         *
     628         * @since 4.0.0
     629         *
     630         * @param WP_Error $error Parse failure.
     631         * @return int HTTP status code, or 0.
     632         */
     633        private static function failure_response_code( $error ) {
     634                $data = $error->get_error_data();
     635                return is_array( $data ) && isset( $data['response_code'] ) ? (int) $data['response_code'] : 0;
     636        }
     637
     638        /**
     639         * Returns how long to wait before retrying a failed citation, if at all.
     640         *
     641         * Temporary failures are retried after a minute, then after 15 minutes, up
     642         * to ENRICH_ATTEMPTS attempts in all. A missing or refused page is retried
     643         * once, after some hours, in case the cited site was down or misconfigured.
     644         * Other failures are not retried.
     645         *
     646         * @since 4.0.0
     647         *
     648         * @param int      $post_id Post ID.
     649         * @param string   $url     Cited URL.
     650         * @param WP_Error $error   The failure.
     651         * @param int      $attempt Which attempt failed.
     652         * @return int Seconds to wait, or 0 for no retry.
     653         */
     654        public static function retry_delay( $post_id, $url, $error, $attempt ) {
     655                switch ( self::failure_type( $error ) ) {
     656                        case 'temporary':
     657                                if ( $attempt >= self::ENRICH_ATTEMPTS ) {
     658                                        return 0;
     659                                }
     660                                return 1 === $attempt ? MINUTE_IN_SECONDS : 15 * MINUTE_IN_SECONDS;
     661                        case 'unavailable':
     662                                // Only once per citation.
     663                                foreach ( self::get_citation_log( $post_id ) as $entry ) {
     664                                        if ( $url === $entry['url'] && 'unavailable' === $entry['type'] ) {
     665                                                return 0;
    217666                                        }
    218667                                }
    219                         }
    220                 }
    221                 return $input;
     668                                /**
     669                                 * Filters how long to wait before retrying, once, a citation whose page
     670                                 * was missing or refused (HTTP 404, 401 or 403).
     671                                 *
     672                                 * @since 4.0.0
     673                                 *
     674                                 * @param int      $delay Delay in seconds. Default 6 hours.
     675                                 * @param string   $url   Cited URL.
     676                                 * @param WP_Error $error The failure.
     677                                 */
     678                                return max( 0, (int) apply_filters( 'post_kinds_micropub_unavailable_retry_delay', 6 * HOUR_IN_SECONDS, $url, $error ) );
     679                        default:
     680                                return 0;
     681                }
     682        }
     683
     684        /**
     685         * Reports a citation that could not be parsed. Only shown when WP_DEBUG is on.
     686         *
     687         * @since 4.0.0
     688         *
     689         * @param string   $url   Cited URL.
     690         * @param WP_Error $error Parse failure.
     691         */
     692        public static function report_failure( $url, $error ) {
     693                wp_trigger_error(
     694                        __METHOD__,
     695                        sprintf( 'Could not parse %1$s for a Micropub citation: %2$s', esc_url( $url ), esc_html( $error->get_error_message() ) )
     696                );
     697        }
     698
     699        /**
     700         * Logs and, where worthwhile, schedules a retry for citations that could not
     701         * be parsed during a Micropub create or update request, once the post ID is known.
     702         *
     703         * @since 4.0.0
     704         *
     705         * @param array      $input   Micropub request.
     706         * @param array|null $wp_args Arguments of the created or updated post.
     707         */
     708        public static function schedule_enrichment( $input, $wp_args ) {
     709                $pending       = self::$pending;
     710                self::$pending = array();
     711                if ( empty( $pending ) || empty( $wp_args['ID'] ) ) {
     712                        return;
     713                }
     714                foreach ( $pending as $citation ) {
     715                        self::handle_failure( (int) $wp_args['ID'], $citation[0], $citation[1], 1, $citation[2] );
     716                }
     717        }
     718
     719        /**
     720         * Logs a failed attempt and schedules a retry if one is worthwhile.
     721         *
     722         * @since 4.0.0
     723         *
     724         * @param int      $post_id  Post ID.
     725         * @param string   $property Micropub property, such as in-reply-to.
     726         * @param string   $url      Cited URL.
     727         * @param int      $attempt  Which attempt failed.
     728         * @param WP_Error $error    The failure.
     729         */
     730        private static function handle_failure( $post_id, $property, $url, $attempt, $error ) {
     731                // Work out the delay before logging, so the log holds only earlier failures.
     732                $delay    = self::retry_delay( $post_id, $url, $error, $attempt );
     733                $retry_at = $delay ? time() + $delay : 0;
     734                self::log_outcome( $post_id, $property, $url, $attempt, $error, $retry_at );
     735                if ( $retry_at ) {
     736                        $args = array( $post_id, $property, $url, $attempt + 1 );
     737                        if ( ! wp_next_scheduled( self::ENRICH_EVENT, $args ) ) {
     738                                wp_schedule_single_event( $retry_at, self::ENRICH_EVENT, $args );
     739                        }
     740                }
     741        }
     742
     743        /**
     744         * Parses a Micropub citation in the background and stores the result.
     745         *
     746         * The citation is only replaced if the post still holds the URL as it was
     747         * stored, so later edits are not overwritten.
     748         *
     749         * @since 4.0.0
     750         *
     751         * @param int    $post_id  Post ID.
     752         * @param string $property Micropub property, such as in-reply-to.
     753         * @param string $url      Cited URL.
     754         * @param int    $attempt  Which attempt this is.
     755         */
     756        public static function enrich_citation( $post_id, $property, $url, $attempt ) {
     757                if ( ! get_post( $post_id ) || ! class_exists( '\\ParseThis\\Parser' ) || ! self::enrichment_enabled() ) {
     758                        return;
     759                }
     760                $key    = 'mf2_' . $property;
     761                $values = get_post_meta( $post_id, $key, true );
     762                $index  = is_array( $values ) ? array_search( $url, $values, true ) : false;
     763                if ( false === $index ) {
     764                        self::log_outcome( $post_id, $property, $url, $attempt, 'skipped', 0 );
     765                        return;
     766                }
     767                $cite = self::parse_citation( $url );
     768                if ( is_wp_error( $cite ) ) {
     769                        self::report_failure( $url, $cite );
     770                        self::handle_failure( $post_id, $property, $url, $attempt, $cite );
     771                        return;
     772                }
     773                $values[ $index ] = $cite;
     774                update_post_meta( $post_id, $key, $values );
     775                self::log_outcome( $post_id, $property, $url, $attempt, 'enriched', 0 );
     776        }
     777
     778        /**
     779         * Returns the log of citation parsing outcomes for a post, oldest first.
     780         *
     781         * Each entry is an array with these keys:
     782         * - time          Unix timestamp of the attempt.
     783         * - property      Micropub property, such as in-reply-to.
     784         * - url           Cited URL.
     785         * - attempt       Attempt number; 1 is the attempt during the Micropub request.
     786         * - result        'enriched', 'failed', or 'skipped' (the citation was edited since).
     787         * - type          For failures: 'temporary', 'unavailable' or 'permanent'. Otherwise empty.
     788         * - code          For failures: the WP_Error code. Otherwise empty.
     789         * - response_code For failures: the HTTP status code, or 0.
     790         * - message       For failures: the error message. Otherwise empty.
     791         * - retry         Unix timestamp of the scheduled retry, or 0 if none.
     792         *
     793         * @since 4.0.0
     794         *
     795         * @param int $post_id Post ID.
     796         * @return array[] Log entries.
     797         */
     798        public static function get_citation_log( $post_id ) {
     799                $log = get_post_meta( $post_id, self::LOG_META, true );
     800                return is_array( $log ) ? $log : array();
     801        }
     802
     803        /**
     804         * Adds an outcome to a post's citation log and updates its citation status.
     805         *
     806         * @since 4.0.0
     807         *
     808         * @param int             $post_id  Post ID.
     809         * @param string          $property Micropub property.
     810         * @param string          $url      Cited URL.
     811         * @param int             $attempt  Attempt number.
     812         * @param WP_Error|string $result   The failure, or 'enriched' or 'skipped'.
     813         * @param int             $retry_at Timestamp of the scheduled retry, or 0.
     814         */
     815        private static function log_outcome( $post_id, $property, $url, $attempt, $result, $retry_at ) {
     816                $failed = is_wp_error( $result );
     817                $entry  = array(
     818                        'time'          => time(),
     819                        'property'      => $property,
     820                        'url'           => $url,
     821                        'attempt'       => (int) $attempt,
     822                        'result'        => $failed ? 'failed' : $result,
     823                        'type'          => $failed ? self::failure_type( $result ) : '',
     824                        'code'          => $failed ? $result->get_error_code() : '',
     825                        'response_code' => $failed ? self::failure_response_code( $result ) : 0,
     826                        'message'       => $failed ? $result->get_error_message() : '',
     827                        'retry'         => (int) $retry_at,
     828                );
     829                $log    = self::get_citation_log( $post_id );
     830                $log[]  = $entry;
     831                $log    = array_slice( $log, -self::LOG_LIMIT );
     832                update_post_meta( $post_id, self::LOG_META, $log );
     833
     834                // The status reflects the latest outcome for each cited URL.
     835                $latest = array();
     836                foreach ( $log as $item ) {
     837                        $latest[ $item['url'] ] = $item;
     838                }
     839                $status = '';
     840                foreach ( $latest as $item ) {
     841                        if ( 'failed' !== $item['result'] ) {
     842                                continue;
     843                        }
     844                        if ( ! $item['retry'] ) {
     845                                $status = 'failed';
     846                                break;
     847                        }
     848                        $status = 'retrying';
     849                }
     850                if ( $status ) {
     851                        update_post_meta( $post_id, self::STATUS_META, $status );
     852                } else {
     853                        delete_post_meta( $post_id, self::STATUS_META );
     854                }
     855
     856                /**
     857                 * Fires after each attempt to parse a Micropub citation.
     858                 *
     859                 * @since 4.0.0
     860                 *
     861                 * @param array $entry   The log entry. See Kind_Plugins::get_citation_log().
     862                 * @param int   $post_id Post ID.
     863                 */
     864                do_action( 'post_kinds_citation_enrichment', $entry, $post_id );
     865        }
     866
     867        /**
     868         * Removes Parse This bookkeeping from parsed jf2, recursively.
     869         *
     870         * Keys starting with an underscore (_code, _links, _alternate,
     871         * _source_format, ...), post-type and refs are not microformats properties,
     872         * and would otherwise be stored in post meta and returned by Micropub's
     873         * q=source.
     874         *
     875         * @since 4.0.0
     876         *
     877         * @access public
     878         *
     879         * @param mixed $jf2 Parsed jf2, or any value within it.
     880         * @return mixed The value without bookkeeping keys.
     881         */
     882        public static function clean_citation( $jf2 ) {
     883                if ( ! is_array( $jf2 ) ) {
     884                        return $jf2;
     885                }
     886                foreach ( array_keys( $jf2 ) as $key ) {
     887                        if ( is_string( $key ) && ( 0 === strpos( $key, '_' ) || in_array( $key, array( 'post-type', 'refs' ), true ) ) ) {
     888                                unset( $jf2[ $key ] );
     889                        } elseif ( is_array( $jf2[ $key ] ) ) {
     890                                $jf2[ $key ] = self::clean_citation( $jf2[ $key ] );
     891                        }
     892                }
     893                return $jf2;
     894        }
     895        /**
     896         * Adds the enabled kinds to Micropub's configuration query, using the
     897         * post-types extension (Query for Supported Vocabulary), so clients only
     898         * offer the kinds this site uses.
     899         *
     900         * @since 4.0.0
     901         *
     902         * @see https://indieweb.org/Micropub-extensions#Query_for_Supported_Vocabulary
     903         *
     904         * @param array|mixed $response Micropub query response.
     905         * @param array       $input    Micropub query.
     906         * @return array|mixed The response, with post-types added for q=config.
     907         */
     908        public static function micropub_query( $response, $input ) {
     909                if ( ! is_array( $response ) || ! isset( $input['q'] ) || 'config' !== $input['q'] ) {
     910                        return $response;
     911                }
     912                $types = isset( $response['post-types'] ) && is_array( $response['post-types'] ) ? $response['post-types'] : array();
     913                $known = wp_list_pluck( $types, 'type' );
     914                foreach ( self::micropub_post_types() as $type ) {
     915                        if ( ! in_array( $type['type'], $known, true ) ) {
     916                                $types[] = $type;
     917                        }
     918                }
     919                $response['post-types'] = $types;
     920                return $response;
     921        }
     922
     923        /**
     924         * Returns the enabled kinds as Micropub post types.
     925         *
     926         * @since 4.0.0
     927         *
     928         * @return array[] Arrays with the keys type (the kind slug, which is its Post
     929         *                 Type Discovery type) and name (the kind's singular name).
     930         */
     931        public static function micropub_post_types() {
     932                // The kinds offered in the editor: those enabled in settings, plus note,
     933                // which is always available.
     934                $kinds = Kind_Config::get_termslist();
     935                array_unshift( $kinds, 'note' );
     936                $types = array();
     937                foreach ( array_unique( $kinds ) as $kind ) {
     938                        $name = Kind_Taxonomy::get_kind_info( $kind, 'singular_name' );
     939                        if ( $name ) {
     940                                $types[] = array(
     941                                        'type' => $kind,
     942                                        'name' => $name,
     943                                );
     944                        }
     945                }
     946                /**
     947                 * Filters the post types advertised to Micropub clients in q=config.
     948                 *
     949                 * @since 4.0.0
     950                 *
     951                 * @param array[] $types Arrays with the keys type and name.
     952                 */
     953                return apply_filters( 'post_kinds_micropub_post_types', $types );
    222954        }
    223955} // End Class Kind_Plugins
  • indieweb-post-kinds/trunk/includes/class-kind-post-widget.php

    r3001401 r3738687  
    11<?php
     2/**
     3 * Kind_Post_Widget class.
     4 *
     5 * @package Post_Kinds
     6 * @since   3.1.5
     7 */
    28
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Widget that lists recent posts of selected kinds.
     13 *
     14 * @since 3.1.5
     15 */
    316class Kind_Post_Widget extends WP_Widget {
    417        /**
    518         * Register widget with WordPress.
     19         *
     20         * @since 3.1.5
    621         */
    722        public function __construct() {
    823                parent::__construct(
    9                         'Kind_Post_Widget',                // Base ID
    10                         __( 'Kind Post Widget', 'indieweb-post-kinds' ),        // Name
     24                        'Kind_Post_Widget',                // Base ID.
     25                        __( 'Kind Post Widget', 'indieweb-post-kinds' ),        // Name.
    1126                        array(
    1227                                'classname'             => 'kind_post_widget',
    … …  
    2035         * Front-end display of widget.
    2136         *
     37         * @since 3.1.5
     38         *
    2239         * @see WP_Widget::widget()
    2340         *
    … …  
    2643         */
    2744        public function widget( $args, $instance ) {
    28                 /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
    29                 $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
    30 
    31                 $kind = $instance['kind'] ?? 'note';
    32                 // phpcs:ignore
    33                 echo $args['before_widget'];
    34                 if ( ! empty( $instance['title'] ) ) {
    35                         echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore
    36                 }
    37                 $transient = get_transient( 'kind_post_widget' );
    38                 if ( false === $transient ) {
    39                                 $query = array(
    40                                         'tax_query'   => array(
    41                                                 array(
    42                                                         'taxonomy' => 'kind',
    43                                                         'field'    => 'slug',
    44                                                         'terms'    => $kind,
    45                                                 ),
     45                $kind   = $instance['kind'] ?? 'note';
     46                $number = absint( $instance['number'] ?? 5 );
     47                $posts  = get_posts(
     48                        array(
     49                                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Kinds are a taxonomy; this is the indexed way to select them.
     50                                'tax_query'           => array(
     51                                        array(
     52                                                'taxonomy' => 'kind',
     53                                                'field'    => 'slug',
     54                                                'terms'    => $kind,
    4655                                        ),
    47                                         'numberposts' => $instance['number'] ?? 5,
    48                                 );
    49                                 $posts = get_posts( $query );
     56                                ),
     57                                'numberposts'         => $number ? $number : 5,
     58                                'no_found_rows'       => true,
     59                                'ignore_sticky_posts' => true,
     60                        )
     61                );
     62                // Like core's Recent Posts widget, show nothing when there are no posts.
     63                // WP_Query caches the result in the object cache, so no transient is needed.
     64                if ( empty( $posts ) ) {
     65                        return;
    5066                }
    5167
    52                 if ( 0 === count( $posts ) ) {
    53                         return;
     68                /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
     69                $title = apply_filters( 'widget_title', $instance['title'] ?? '', $instance, $this->id_base );
     70
     71                echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper markup registered by the theme.
     72                if ( $title ) {
     73                        echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Wrapper markup is the theme's; the title is escaped by core's widget_title filter.
    5474                }
    55                 echo '<div id="kind-posts">';
    56                 if ( 0 !== count( $posts ) ) {
    57                         echo '<ul>';
    58                         foreach ( $posts as $post ) {
    59                                 printf( '<li>%1$s</li>', kind_get_the_link( $post ) ); // phpcs:ignore
    60                         }
    61                         echo '</ul>';
    62                 } else {
    63                         esc_html_e( 'No Posts Found', 'indieweb-post-kinds' );
     75                echo '<div id="kind-posts"><ul>';
     76                foreach ( $posts as $post ) {
     77                        printf( '<li>%1$s</li>', kind_get_the_link( $post ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- kind_get_the_link() escapes its parts.
    6478                }
    65                 echo '</div>';
    66                 echo $args['after_widget']; // phpcs:ignore
     79                echo '</ul></div>';
     80                echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper markup registered by the theme.
    6781        }
    6882
    6983        /**
    7084         * Sanitize widget form values as they are saved.
     85         *
     86         * @since 3.1.5
    7187         *
    7288         * @see WP_Widget::update()
    … …  
    7894         */
    7995        public function update( $new_instance, $old_instance ) {
    80                 array_walk_recursive( $new_instance, 'sanitize_text_field' );
    81                 return $new_instance;
     96                $kind   = isset( $new_instance['kind'] ) && is_string( $new_instance['kind'] ) ? sanitize_key( $new_instance['kind'] ) : '';
     97                $number = isset( $new_instance['number'] ) ? absint( $new_instance['number'] ) : 0;
     98                return array(
     99                        'title'  => isset( $new_instance['title'] ) && is_string( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '',
     100                        'kind'   => in_array( $kind, Kind_Taxonomy::get_kind_list(), true ) ? $kind : 'note',
     101                        'number' => $number > 0 ? $number : 5,
     102                );
    82103        }
    83104
    … …  
    85106        /**
    86107         * Create the form for the Widget admin
     108         *
     109         * @since 3.1.5
    87110         *
    88111         * @see WP_Widget::form()
    … …  
    93116                $instance['kind'] = ( $instance['kind'] ?? 'note' );
    94117                ?>
    95                                 <p><label for="title"><?php esc_html_e( 'Title: ', 'indieweb-post-kinds' ); ?></label>
    96                                 <input type="text" size="30" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?> id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
    97                                 value="<?php echo esc_html( $instance['title'] ?? '' ); ?>" /></p>
     118                                <p><label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title: ', 'indieweb-post-kinds' ); ?></label>
     119                                <input type="text" size="30" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
     120                                value="<?php echo esc_attr( $instance['title'] ?? '' ); ?>" /></p>
    98121                <select name="<?php echo esc_attr( $this->get_field_name( 'kind' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'kind' ) ); ?>">
    99122                <?php
    100                 $list   = get_option( 'kind_termslist', Kind_Taxonomy::get_kind_list() );
     123                $list   = Kind_Config::get_termslist();
    101124                $list[] = 'note';
     125                $list   = array_unique( $list );
    102126                foreach ( $list as $term ) {
    103                         $value = Kind_Taxonomy::get_post_kind_info( $term );
    104127                        printf(
    105128                                '<option value="%1$s" %3$s>%2$s</option>',
    106129                                esc_attr( $term ),
    107                                 Kind_Taxonomy::get_kind_info( $term, 'singular_name' ), // phpcs:ignore
    108                                 selected( $instance['kind'], $term )
     130                                esc_html( Kind_Taxonomy::get_kind_info( $term, 'singular_name' ) ),
     131                                selected( $instance['kind'], $term, false )
    109132                        );
    110                                 printf( '%1$s %2$s<br />', Kind_Taxonomy::get_icon( $term ), esc_html( $value->singular_name ) ); //phpcs:ignore
    111133                }
    112134                ?>
  • indieweb-post-kinds/trunk/includes/class-kind-post.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Kind Post Class
     3 * Kind_Post class.
    44 *
    5  * @package Post Kinds
    6  * Assists in retrieving/saving properties from a Post. Replaces MF2_Post by only looking for Items on Demand instead of Parsing Them initially.
     5 * @package Post_Kinds
     6 * @since   3.4.0
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Reads and writes the kind properties of a post.
     13 *
     14 * Replaces MF2_Post by looking up each property on demand instead of parsing
     15 * all of them up front.
     16 *
     17 * @since 3.4.0
    718 */
    819class Kind_Post {
    920
     21        /**
     22         * Post ID.
     23         *
     24         * @since 3.4.0
     25         * @var int
     26         */
    1027        public $id;
    1128
     29        /**
     30         * Sets up the post to read and write.
     31         *
     32         * @since 3.4.0
     33         *
     34         * @param int|WP_Post|string $post Post ID, post object, or the URL of a post or attachment.
     35         */
    1236        public function __construct( $post ) {
    1337                if ( is_numeric( $post ) ) {
    … …  
    2145                                $post     = $id;
    2246                        } else {
    23                                 $id       = attachment_url_to_postid( $post );
     47                                $id       = self::get_attachment_id( $post );
    2448                                $this->id = $id;
    2549                                $post     = $id;
    … …  
    3054        }
    3155
    32         /*
    33          * Check is post is attachment.
    34          *
    35          * @param WP_Post $post Post Object.
    36          * @return boolean True if attachment.
     56        /**
     57         * Checks whether a post is an attachment.
     58         *
     59         * @since 3.4.10
     60         *
     61         * @param int|WP_Post $post Post ID or post object.
     62         * @return bool True if the post is an attachment.
    3763         */
    3864        private function is_attachment( $post ) {
    … …  
    4167
    4268
    43         /*
    44          * Returns WP_Post Object
    45          *
    46          * @return WP_Post Post Object.
    47          *
     69        /**
     70         * Returns the post object.
     71         *
     72         * @since 3.4.0
     73         *
     74         * @return WP_Post|null The post, or null if it does not exist.
    4875         */
    4976        public function get_post() {
    … …  
    5178        }
    5279
    53         /*
    54          * Returns the Post Kind. For Attachments will return the media type.
    55          *
    56          * @return string Post Kind.
    57          *
     80        /**
     81         * Returns the post kind. For an attachment, returns its media type instead.
     82         *
     83         * @since 3.4.0
     84         *
     85         * @return string|false|null The kind slug ('photo', 'video' or 'audio' for an attachment),
     86         *                           false if a post has no kind, or null for an attachment of another type.
    5887         */
    5988        public function get_kind() {
    … …  
    73102        }
    74103
    75         /*
    76          * Get Name.
    77          *
    78          * @return string Return name.
     104        /**
     105         * Returns the post's name (title).
     106         *
     107         * A title that is just the post ID, or an attachment title that is just its
     108         * file name, does not count as a name.
     109         *
     110         * @since 3.4.0
     111         *
     112         * @return string|false The name, or false if there is none.
    79113         */
    80114        public function get_name() {
    … …  
    102136        }
    103137
    104         /*
    105          * Get Content.
    106          *
    107          * @return array with HTML and Plaintext Version of Content or Summary
     138        /**
     139         * Returns the post's content or summary (excerpt).
     140         *
     141         * @since 3.4.0
     142         *
     143         * @param string $property Either 'content' or 'summary'.
     144         * @return array|false Array with the HTML ('html') and plain text ('value') versions,
     145         *                     or false if the property is empty or not supported.
    108146         */
    109147        public function get_html( $property ) {
    … …  
    124162        }
    125163
    126         /*
    127          * Get Permalink URL
    128          *
    129          * @return string $url
     164        /**
     165         * Returns the post's permalink, or the file URL for an attachment.
     166         *
     167         * @since 3.4.0
     168         *
     169         * @return string|false The URL, or false if there is none.
    130170         */
    131171        public function get_url() {
    … …  
    137177        }
    138178
    139         /*
    140          * Get Featured Image Permalink
    141          *
    142          * @return string $url
     179        /**
     180         * Returns the URL of the post's featured image.
     181         *
     182         * @since 3.4.0
     183         *
     184         * @return string|false The URL, or false if the post has no featured image.
    143185         */
    144186        public function get_featured() {
    … …  
    150192
    151193
    152         /*
    153          * Return datetime property as a DateTime Object.
    154          *
    155          * @param string $property Property You Wish to Return.
    156          * @return DateTimeImmutable Published Time in Local Timezone.
    157          *
     194        /**
     195         * Returns a date property as a date object.
     196         *
     197         * For posts other than attachments, 'published' and 'updated' come from the
     198         * post's own dates. Everything else comes from post meta.
     199         *
     200         * @since 3.4.0
     201         *
     202         * @param string $property Property to return, such as 'published' or 'start'.
     203         * @return DateTimeImmutable|false The date, or false if it is missing or not a valid date.
    158204         */
    159205        public function get_datetime_property( $property ) {
    … …  
    175221                        $datetime = $datetime[0];
    176222                }
    177                 return new DateTimeImmutable( $datetime );
    178         }
    179 
    180         /*
    181          * Returns Publication.
    182          *
    183          * @return string
    184          *
     223                return kind_safe_datetime( $datetime );
     224        }
     225
     226        /**
     227         * Returns the publication the post appeared in.
     228         *
     229         * For posts other than attachments, this is the site title.
     230         *
     231         * @since 3.4.0
     232         *
     233         * @return string|null The publication, or null if an attachment has none.
    185234         */
    186235        public function get_publication() {
    … …  
    195244        }
    196245
     246        /**
     247         * Returns the post's stored duration.
     248         *
     249         * @since 3.4.0
     250         *
     251         * @return DateInterval|false The duration, or false if it is missing or not valid.
     252         */
    197253        public function get_duration() {
    198254                $duration = get_post_meta( $this->id, 'mf2_duration', true );
    … …  
    200256                        $duration = $duration[0];
    201257                }
    202                 if ( $duration ) {
    203                         return new DateInterval( $duration );
    204                 }
    205                 return false;
    206         }
    207 
    208         /*
    209          * Return Categories which are a combination of Tags and Categories.
    210          *
    211          * @return array Array of the names of Categories and Tag names.
    212          *
     258                return kind_safe_interval( $duration );
     259        }
     260
     261        /**
     262         * Returns the names of the post's categories and tags, combined.
     263         *
     264         * 'Uncategorized' is left out.
     265         *
     266         * @since 3.4.0
     267         *
     268         * @return string[] Category and tag names.
    213269         */
    214270        public function get_categories() {
    215271                $category = array();
    216                 // Get a list of categories and extract their names
     272                // Get a list of categories and extract their names.
    217273                $post_categories = get_the_terms( $this->id, 'category' );
    218274                if ( ! empty( $post_categories ) && ! is_wp_error( $post_categories ) ) {
    … …  
    220276                }
    221277
    222                 // Get a list of tags and extract their names
     278                // Get a list of tags and extract their names.
    223279                $post_tags = get_the_terms( $this->id, 'post_tag' );
    224280                if ( ! empty( $post_tags ) && ! is_wp_error( $post_tags ) ) {
    225                         $category = array_merge( $this->category, wp_list_pluck( $post_tags, 'name' ) );
     281                        $category = array_merge( $category, wp_list_pluck( $post_tags, 'name' ) );
    226282                }
    227283                if ( in_array( 'Uncategorized', $category, true ) ) {
    … …  
    232288
    233289        /**
    234          * Is prefix in string.
    235          *
    236          * @param  string $source The source string.
    237          * @param  string $prefix The prefix you wish to check for in source.
    238          * @return boolean The result.
     290         * Checks whether a string starts with a prefix.
     291         *
     292         * @since 3.4.0
     293         *
     294         * @param string $source The source string.
     295         * @param string $prefix The prefix to look for.
     296         * @return bool True if the source starts with the prefix.
    239297         */
    240298        protected static function str_prefix( $source, $prefix ) {
    … …  
    243301
    244302        /**
    245          * Returns True if Array is Multidimensional.
    246          *
    247          * @param array $arr array.
    248          *
    249          * @return boolean result
     303         * Checks whether an array is multidimensional.
     304         *
     305         * @since 3.4.0
     306         *
     307         * @param array $arr The array to check.
     308         * @return bool True if any element is an array.
    250309         */
    251310        protected static function is_multi_array( $arr ) {
    … …  
    257316        }
    258317
     318        /**
     319         * Unwraps single-item arrays, recursively.
     320         *
     321         * @since 3.4.0
     322         *
     323         * @param mixed $value   The value to unwrap.
     324         * @param bool  $discard Optional. Whether to keep only the first item of a list
     325         *                       with more than one item. Default false.
     326         * @return mixed The value, with single-item arrays replaced by their item.
     327         */
    259328        protected function single_array( $value, $discard = false ) {
    260329                if ( ! is_array( $value ) ) {
    … …  
    274343
    275344        /**
    276          * Retrieve author
    277          *
    278          * @return boolean|array The result or false if does not exist.
     345         * Returns the post's author as an h-card.
     346         *
     347         * For an attachment, or a post with no WordPress author, this is the author
     348         * stored in post meta.
     349         *
     350         * @since 3.4.0
     351         *
     352         * @return array|string|false The author as mf2, the stored value, or false if there is none.
    279353         */
    280354        public function get_author() {
    … …  
    284358                        return ( $author ?? false );
    285359                }
    286                 // Attachments may have been uploaded by a user but may have metadata for original author
     360                // Attachments may have been uploaded by a user but may have metadata for original author.
    287361                if ( 'attachment' === get_post_type( $this->id ) ) {
    288362                        $author = get_post_meta( $post->ID, 'mf2_author', true );
    … …  
    294368                                'name'  => array( get_the_author_meta( 'display_name', $post->post_author ) ),
    295369                                'url'   => array( get_the_author_meta( 'user_url', $post->post_author ) ? get_the_author_meta( 'user_url', $post->post_author ) : get_author_posts_url( $post->post_author ) ),
    296                                 'photo' => array( get_avatar_url( $this->post_author ) ),
     370                                'photo' => array( get_avatar_url( $post->post_author ) ),
    297371                        ),
    298372                );
    299373        }
    300374
    301         /*
    302          * Return Attached Media IDs.
    303          *
    304          * @param string $type audio, video, or photo.
    305          * @return array Array of Media IDs.
    306          *
     375        /**
     376         * Returns the IDs of media attached to the post.
     377         *
     378         * @since 3.4.0
     379         *
     380         * @param string $type 'audio', 'video' or 'photo'.
     381         * @return int[]|false Attachment IDs, or false if the type is not supported.
    307382         */
    308383        public function get_attached_media( $type ) {
    … …  
    318393        }
    319394
    320         /*
    321          * Return Attached Photos.
    322          *
    323          * Looks in both attached media and the photo property.
    324          *
    325          * @param boolean $content If true then return empty if there are any images in content.
    326          * @return array Array of Media IDs.
    327          *
     395        /**
     396         * Returns the post's photos.
     397         *
     398         * Looks in attached media, the photo property and images in the content.
     399         * Returns an empty array if the post has a featured image.
     400         *
     401         * @since 3.4.0
     402         *
     403         * @param bool $content Optional. Whether to return an empty array if the content
     404         *                      has any images. Default true.
     405         * @return int[]|false Attachment IDs, or false if there are none.
    328406         */
    329407        public function get_photo( $content = true ) {
     408                if ( ! $this->get_post() ) {
     409                        return false;
     410                }
    330411                // Check if the post itself is an image attachment.
    331412                if ( wp_attachment_is( 'image', $this->id ) ) {
    … …  
    333414                }
    334415
     416                // Recorded when the post is saved; see Kind_Media_Metadata::save_post().
    335417                $content_ids = get_post_meta( $this->id, '_content_img_ids', true );
    336 
    337                 if ( false === $content_ids ) {
    338                         // Check for a gallery first.
    339                         $gallery = get_post_gallery( $this->id, false );
    340                         if ( $gallery & array_key_exists( 'ids', $gallery ) ) {
    341                                 $content_ids = explode( ',', $gallery['ids'] );
    342                         }
    343                         $content_ids = array();
    344                         $post        = $this->get_post();
    345                         $shortcode   = false;
    346                         $shortcodes  = apply_filter( 'kind_photo_shortcode_exclude', array( 'vr', '360' ) );
    347                         if ( $post->post_content ) {
    348                                 foreach ( $shortcodes as $code ) {
    349                                         if ( has_shortcode( $post->post_content, $code ) ) {
    350                                                 $shortcode = true;
    351                                         }
    352                                 }
    353                                 if ( $shortcode ) {
    354                                         $content_ids = array();
    355                                 } else {
    356                                         $content_ids = array_merge( $content_ids, Kind_Media_Metadata::get_img_from_content( $post->post_content ) );
    357                                 }
    358                                 update_post_meta( $this->id, '_content_img_ids', $content_ids );
    359                         }
    360                 }
    361418
    362419                // If there are photos in the content then end here if this is true.
    … …  
    393450        }
    394451
    395         /*
    396          * Return Attached Audio.
    397          *
    398          * Looks in both attached media and the audio property.
    399          *
    400          * @param boolean $content If true then return empty if there are any audio files in content.
    401          * @return array Array of Media IDs or URLs.
    402          *
     452        /**
     453         * Returns the post's audio.
     454         *
     455         * Looks in attached media, the audio property and audio in the content.
     456         *
     457         * @since 3.4.0
     458         *
     459         * @param bool $content Optional. Whether to return an empty array if the content
     460         *                      has any audio. Default true.
     461         * @return array|false Attachment IDs, external URLs if no attachments were found,
     462         *                     or false if there is no audio.
    403463         */
    404464        public function get_audio( $content = true ) {
    … …  
    408468                }
    409469
     470                // Recorded when the post is saved; see Kind_Media_Metadata::save_post().
    410471                $content_ids = get_post_meta( $this->id, '_content_audio_ids', true );
    411 
    412                 if ( false === $content_ids ) {
    413                         $post = get_post();
    414                         if ( $post->post_content ) {
    415                                 $content_ids = Kind_Media_Metadata::get_audio_from_content( $post->post_content );
    416                                 update_post_meta( $this->id, '_content_audio_ids', $content_ids );
    417                         }
    418                 }
    419472
    420473                // If there are ids in the content then end here if this is true.
    … …  
    431484                $audio_ids = is_array( $audios ) ? $this->get_attachments_from_urls( $audios ) : array();
    432485
    433                 // If there are ids found return them
     486                // If there are ids found return them.
    434487                if ( ! empty( $audio_ids ) || ! empty( $att_ids ) || ! empty( $content_ids ) ) {
    435                         return array_unique( array_merge( $att_ids, $this->get_attachments_from_urls( $audios ), $content_ids ) );
     488                        return array_unique( array_merge( $att_ids, $audio_ids, $content_ids ) );
    436489                }
    437490
    … …  
    444497        }
    445498
    446         /*
    447          * Return Attached Video.
    448          *
    449          * Looks in both attached media and the audio property.
    450          *
    451          * @param boolean $content If true then return empty if there are any video files in content.
    452          * @return array Array of Media IDs.
    453          *
     499        /**
     500         * Returns the post's videos.
     501         *
     502         * Looks in attached media, the video property and videos in the content.
     503         *
     504         * @since 3.4.0
     505         *
     506         * @param bool $content Optional. Whether to return an empty array if the content
     507         *                      has any videos. Default true.
     508         * @return int[]|false Attachment IDs, or false if there are none.
    454509         */
    455510        public function get_video( $content = true ) {
    … …  
    458513                        return array( $this->id );
    459514                }
     515                // Recorded when the post is saved; see Kind_Media_Metadata::save_post().
    460516                $content_ids = get_post_meta( $this->id, '_content_video_ids', true );
    461 
    462                 if ( false === $content_ids ) {
    463                         $post = get_post();
    464                         if ( $post->post_content ) {
    465                                 $content_ids = Kind_Media_Metadata::get_video_from_content( $post->post_content );
    466                                 update_post_meta( $this->id, '_content_video_ids', $content_ids );
    467                         }
    468                 }
    469517
    470518                // If there are ids in the content then end here if this is true.
    … …  
    488536        }
    489537
     538        /**
     539         * Look up the attachment ID for a URL, with caching.
     540         *
     541         * Wraps attachment_url_to_postid(), which runs an uncached query, in the
     542         * object cache. The key includes the posts last_changed value, so adding,
     543         * changing or deleting any post or attachment invalidates it. Without a
     544         * persistent object cache this still avoids repeat lookups in a request.
     545         *
     546         * @since 4.0.0
     547         *
     548         * @param string $url Media URL.
     549         * @return int Attachment ID, or 0 if there is none.
     550         */
     551        public static function get_attachment_id( $url ) {
     552                if ( ! is_string( $url ) || '' === $url ) {
     553                        return 0;
     554                }
     555                $key = 'attachment_id:' . md5( $url ) . ':' . wp_cache_get_last_changed( 'posts' );
     556                $id  = wp_cache_get( $key, 'post-kinds' );
     557                if ( false === $id ) {
     558                        $id = attachment_url_to_postid( $url );
     559                        wp_cache_set( $key, $id, 'post-kinds' );
     560                }
     561                return (int) $id;
     562        }
     563
     564        /**
     565         * Looks up the attachment IDs for media URLs.
     566         *
     567         * @since 3.4.0
     568         *
     569         * @param string|array $urls A URL, or a list of URLs, attachment IDs or arrays with a 'url' key.
     570         * @return int[] Attachment IDs found. URLs with no attachment are left out.
     571         */
    490572        public function get_attachments_from_urls( $urls ) {
    491573                if ( is_string( $urls ) ) {
    492                         $attachment = attachment_url_to_postid( $urls );
     574                        $attachment = self::get_attachment_id( $urls );
    493575                        if ( $attachment ) {
    494576                                return array( $attachment );
    … …  
    502584                                if ( is_array( $url ) ) {
    503585                                        if ( isset( $url['url'] ) ) {
    504                                                 $att_ids[] = attachment_url_to_postid( $url['url'] );
     586                                                $att_ids[] = self::get_attachment_id( $url['url'] );
    505587                                        }
    506588                                } elseif ( is_numeric( $url ) ) {
    507589                                        $att_ids[] = $url;
    508590                                } else {
    509                                         $att_ids[] = attachment_url_to_postid( $url );
     591                                        $att_ids[] = self::get_attachment_id( $url );
    510592                                }
    511593                        }
    … …  
    515597
    516598        /**
    517          * Retrieve value
    518          *
    519          * @param  string $key The key to retrieve.
    520          * @param  boolean $single Whether to return a a single value or array if there is only one value.
    521          * @return mixed The result or false if does not exist.
     599         * Returns a property of the post.
     600         *
     601         * @since 3.4.0
     602         *
     603         * @param string $key    The property to return.
     604         * @param bool   $single Optional. Whether to unwrap a single value from its array. Default true.
     605         * @return mixed The value, or false if the key is empty.
    522606         */
    523607        public function get( $key, $single = true ) {
    … …  
    559643        }
    560644
     645        /**
     646         * Returns the post's citation, or one property of it.
     647         *
     648         * For an attachment, the citation is built from the attachment itself.
     649         * Citations stored as jf2 are returned as mf2.
     650         *
     651         * @since 3.4.0
     652         *
     653         * @param string|null $key Optional. Citation property to return. Default null, for the whole citation.
     654         * @return mixed The citation as mf2 or a string, the property, or false if there is none.
     655         */
    561656        public function get_cite( $key = null ) {
    562657                if ( 'attachment' === get_post_type( $this->id ) ) {
    … …  
    569664                                $duration = date_interval_to_iso8601( $duration );
    570665                        }
    571                         $cite = jf2_to_mf2(
     666                        $cite = \ParseThis\jf2_to_mf2(
    572667                                array_filter(
    573668                                        array(
    … …  
    604699                                        $cite = $cite[0];
    605700                                }
    606                                 $this->delete( 'cite' );
    607                         }
    608                 }
    609 
    610                 // If this is formatted as JF2 try to convert it to MF2 and update.
    611                 if ( is_array( $cite ) && ! wp_is_numeric_array( $cite ) ) {
    612                         $cite['type'] = 'cite';
    613                         $cite         = jf2_to_mf2( $cite );
    614                         $property     = Kind_Taxonomy::get_kind_info( $this->get_kind(), 'property' );
    615                         $this->set( $property, $cite );
     701                        }
     702
     703                        // Citations stored as jf2 are returned as mf2.
     704                        if ( is_array( $cite ) && ! wp_is_numeric_array( $cite ) ) {
     705                                if ( ! isset( $cite['properties'] ) ) {
     706                                        if ( empty( $cite['type'] ) ) {
     707                                                $cite['type'] = $this->get_cite_type();
     708                                        }
     709                                        $cite = \ParseThis\jf2_to_mf2( $cite );
     710                                } elseif ( empty( $cite['type'] ) || ! is_array( $cite['type'] ) ) {
     711                                        // Reads before 4.0.0 stored the type as 'cite'.
     712                                        $cite['type'] = array( 'h-' . $this->get_cite_type() );
     713                                }
     714                        }
    616715                }
    617716
    … …  
    649748        }
    650749
    651         /* Returns a normalized cite with all possible parameters present to reduce isset checks and to ensure everything is formatted correctly
    652          * For Display and Metabox purposes only.
     750        /**
     751         * Returns the jf2 type of the post's citation, based on its kind.
     752         *
     753         * Matches the type Kind_Metabox::save_post() stores: a checkin cites a
     754         * place (card), eat and drink cite food, and every other kind a citation.
     755         *
     756         * @since 4.0.0
     757         *
     758         * @return string The jf2 type: 'card', 'food' or 'cite'.
     759         */
     760        public function get_cite_type() {
     761                $kind = $this->get_kind();
     762                if ( 'checkin' === $kind ) {
     763                        return 'card';
     764                }
     765                if ( in_array( $kind, array( 'drink', 'eat' ), true ) ) {
     766                        return 'food';
     767                }
     768                return 'cite';
     769        }
     770
     771        /**
     772         * Returns a citation as jf2 with every key present and formatted for display.
     773         *
     774         * Missing keys are set to empty strings, which saves isset checks. For
     775         * display and the metabox only.
     776         *
     777         * 'authors' lists every author as a card with name, url and photo. 'author'
     778         * is the single author, or for several authors one card whose fields hold
     779         * each author's value in turn, separated by semicolons, as the metabox
     780         * shows them.
     781         *
     782         * @since 3.4.9
     783         * @since 4.0.0 Added 'authors'. 'author' joins several authors' photos too.
     784         *
     785         * @param array|string|false $cite The citation, as mf2, jf2 or a URL or name.
     786         * @return array The normalized citation.
    653787         */
    654788        public function normalize_cite( $cite ) {
    655                 // Ensures that an empty string is always present in the cite
     789                // Ensures that an empty string is always present in the cite.
    656790                $author_defaults = array(
    657791                        'type'  => 'card',
    … …  
    670804                        'summary'     => '',
    671805                        'author'      => $author_defaults,
     806                        'authors'     => array(),
    672807                        'category'    => '',
    673808                );
    … …  
    680815                        $cite = $cite[0];
    681816                }
    682                 if ( Parse_This_MF2_Utils::is_microformat( $cite ) ) {
    683                         $cite = mf2_to_jf2( $cite );
     817                if ( \ParseThis\MF2_Utils::is_microformat( $cite ) ) {
     818                        $cite = \ParseThis\mf2_to_jf2( $cite );
    684819                }
    685820
    … …  
    698833                }
    699834
    700                 if ( wp_is_numeric_array( $cite['author'] ) ) {
    701                         if ( 1 === count( $cite['author'] ) ) {
    702                                 $cite['author'] = array_pop( $cite['author'] );
    703                         }
    704                 }
    705 
    706                 if ( is_string( $cite['author'] ) ) {
    707                         $cite['author'] = wp_http_validate_url( $cite['author'] ) ? array( 'url' => $cite['author'] ) : array( 'name' => $cite['author'] );
    708                 }
    709 
    710                 $cite['author'] = wp_parse_args( $cite['author'], $author_defaults );
     835                $cite['authors'] = self::normalize_authors( $cite['author'] );
     836                if ( 1 === count( $cite['authors'] ) ) {
     837                        $cite['author'] = $cite['authors'][0];
     838                } else {
     839                        $cite['author'] = $author_defaults;
     840                        foreach ( array( 'name', 'url', 'photo' ) as $key ) {
     841                                $values = wp_list_pluck( $cite['authors'], $key );
     842                                if ( array_filter( $values ) ) {
     843                                        $cite['author'][ $key ] = implode( '; ', $values );
     844                                }
     845                        }
     846                }
    711847
    712848                if ( is_array( $cite['publication'] ) ) {
    713849                        $cite['publication'] = $cite['publication']['name'];
    714                 }
    715 
    716                 if ( is_array( $cite['author']['name'] ) ) {
    717                         $cite['author']['name'] = implode( ';', $cite['author']['name'] );
    718                 }
    719                 if ( is_array( $cite['author']['url'] ) ) {
    720                         $cite['author']['url'] = implode( ';', $cite['author']['url'] );
    721850                }
    722851
    … …  
    733862        }
    734863
     864
     865        /**
     866         * Returns a citation's authors as a list of cards.
     867         *
     868         * Accepts one author or a list, each as a jf2 or mf2 h-card, a URL or a
     869         * name. Before 4.0.0 the metabox saved several authors as one card whose
     870         * name, url and photo were lists; the values are paired up by position.
     871         *
     872         * @since 4.0.0
     873         *
     874         * @param mixed $author The author or authors.
     875         * @return array[] Cards with string type, name, url and photo, leaving out empty ones.
     876         */
     877        public static function normalize_authors( $author ) {
     878                if ( wp_is_numeric_array( $author ) ) {
     879                        $authors = array();
     880                        foreach ( $author as $item ) {
     881                                $authors = array_merge( $authors, self::normalize_authors( $item ) );
     882                        }
     883                        return $authors;
     884                }
     885                if ( is_string( $author ) ) {
     886                        $author = wp_http_validate_url( $author ) ? array( 'url' => $author ) : array( 'name' => $author );
     887                }
     888                if ( ! is_array( $author ) ) {
     889                        return array();
     890                }
     891                if ( \ParseThis\MF2_Utils::is_microformat( $author ) ) {
     892                        $author = \ParseThis\mf2_to_jf2( $author );
     893                }
     894
     895                $fields = array();
     896                foreach ( array( 'name', 'url', 'photo' ) as $key ) {
     897                        $value = isset( $author[ $key ] ) ? $author[ $key ] : '';
     898                        if ( is_array( $value ) && isset( $value['value'] ) ) {
     899                                // A jf2 photo with alt text.
     900                                $value = $value['value'];
     901                        }
     902                        $fields[ $key ] = array_values( array_filter( (array) $value, 'is_scalar' ) );
     903                }
     904
     905                $authors = array();
     906                $count   = max( array_map( 'count', $fields ) );
     907                for ( $i = 0; $i < $count; $i++ ) {
     908                        $card = array( 'type' => 'card' );
     909                        foreach ( $fields as $key => $values ) {
     910                                $card[ $key ] = isset( $values[ $i ] ) ? trim( (string) $values[ $i ] ) : '';
     911                        }
     912                        if ( '' !== $card['name'] . $card['url'] . $card['photo'] ) {
     913                                $authors[] = $card;
     914                        }
     915                }
     916                return $authors;
     917        }
     918        /**
     919         * Stores a date property.
     920         *
     921         * For posts other than attachments, 'published' and 'updated' set the
     922         * post's own dates. Everything else is stored in post meta.
     923         *
     924         * @since 3.4.0
     925         *
     926         * @param string $key   Property to set, such as 'published' or 'start'.
     927         * @param mixed  $value A date, in any form kind_safe_datetime() accepts.
     928         * @return int|bool|WP_Error The result of updating the post or its meta,
     929         *                           or false if the value is not a valid date.
     930         */
    735931        public function set_datetime_property( $key, $value ) {
    736932                // In an attachment the post date properties reflect when the item was uploaded not when the piece was created.
    737                 if ( ! $value instanceof DateTime ) {
    738                         $value = new DateTime( $value );
    739                 }
     933                // A mutable copy, as the timezone is changed below.
     934                $value = kind_safe_datetime( $value, null, false );
    740935                if ( ! $value ) {
    741936                        return false;
    … …  
    755950        }
    756951
     952        /**
     953         * Stores the post's duration as an ISO 8601 duration.
     954         *
     955         * @since 3.4.0
     956         *
     957         * @param mixed $value A duration, in any form kind_safe_interval() accepts.
     958         * @return int|bool The result of updating the meta, or false if the value is not a valid duration.
     959         */
    757960        public function set_duration( $value ) {
    758                 if ( ! $value instanceof DateInterval ) {
    759                         $value = new DateInterval( $value );
    760                 }
     961                $value = kind_safe_interval( $value );
    761962                if ( ! $value ) {
    762963                        return false;
    … …  
    769970
    770971        /**
    771          * Set author
    772          *
    773          * @param array $value Author microformat.
    774          * @return boolean|WP_Error
     972         * Stores the author of an attachment.
     973         *
     974         * Other posts use their WordPress author, so nothing is stored for them.
     975         *
     976         * @since 3.4.0
     977         *
     978         * @param array $value Author, as mf2.
     979         * @return int|bool|null The result of updating the meta, or null if the post is not an attachment.
    775980         */
    776981        public function set_author( $value ) {
    777                 // Attachments may have been uploaded by a user but may have metadata for original author
     982                // Attachments may have been uploaded by a user but may have metadata for original author.
    778983                if ( 'attachment' === get_post_type( $this->id ) ) {
    779984                        return update_post_meta( $this->id, 'mf2_author', $value );
    … …  
    781986        }
    782987
     988        /**
     989         * Stores a property, or several.
     990         *
     991         * @since 3.4.0
     992         *
     993         * @param string|array $key   Property to set, or an array of property => value pairs.
     994         * @param mixed        $value Optional. The value. Default null.
     995         * @return mixed The result of storing the value. True when setting an array,
     996         *               and null if the key or value is empty.
     997         */
    783998        public function set( $key, $value = null ) {
    784999                if ( is_array( $key ) ) {
    … …  
    8041019                        case 'featured':
    8051020                                if ( wp_http_validate_url( $value ) ) {
    806                                         $featured = attachment_url_to_postid( $value );
     1021                                        $featured = self::get_attachment_id( $value );
    8071022                                        if ( $featured ) {
    8081023                                                $value = $featured;
    … …  
    8321047                                return wp_update_post( $args, true );
    8331048                        case 'audio':
    834                                 /* All media is handled identically.
    835                                 */
     1049                                // All media is handled identically.
    8361050                        case 'video':
    837                                 /* All Media is handled identically.
    838                                 */
     1051                                // All media is handled identically.
    8391052                        case 'photo':
    840                                 if ( Parse_This_MF2::is_microformat( $value ) ) {
    841                                         $url = Parse_This_MF2::get_plaintext( $value, 'url' );
    842                                         $id  = attachment_url_to_postid( $url );
     1053                                if ( \ParseThis\MF2_Utils::is_microformat( $value ) ) {
     1054                                        $url = \ParseThis\MF2_Utils::get_plaintext( $value, 'url' );
     1055                                        $id  = self::get_attachment_id( $url );
    8431056                                        if ( $id ) {
    844                                                 $value = mf2_to_jf2( $value );
     1057                                                $value = \ParseThis\mf2_to_jf2( $value );
    8451058                                                unset( $value['type'] );
    8461059                                                $attachment = new Kind_Post( $id );
    … …  
    8521065                                        return update_post_meta( $this->id, 'mf2_' . $key, array( $url ) );
    8531066                                }
    854                                 /* If it is not a microformat handle as default.
    855                                 */
     1067                                // If it is not a microformat handle as default.
    8561068                        default:
    8571069                                return update_post_meta( $this->id, 'mf2_' . $key, $value );
    … …  
    8591071        }
    8601072
     1073        /**
     1074         * Deletes a property stored in post meta.
     1075         *
     1076         * @since 3.4.0
     1077         *
     1078         * @param string $key Property to delete.
     1079         * @return bool True on success, false on failure.
     1080         */
    8611081        public function delete( $key ) {
    8621082                return delete_post_meta( $this->id, 'mf2_' . $key );
  • indieweb-post-kinds/trunk/includes/class-kind-taxonomy.php

    r3011061 r3738687  
    11<?php
    2 
    32/**
    4  * Post Kind Taxonomy Class
     3 * Kind_Taxonomy class.
    54 *
    6  * Registers the taxonomy and sets its behavior.
     5 * @package Post_Kinds
     6 * @since   2.0.2
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Registers the kind taxonomy and sets its behavior.
    713 *
    8  * @package Post Kinds
     14 * @since 2.0.2
    915 */
    1016final class Kind_Taxonomy {
    11         private static $kinds = array(); // Store a Post_Kind class which is a definition of a specific kind
    12 
     17        /**
     18         * Registered kinds, keyed by slug.
     19         *
     20         * @since 3.1.0
     21         * @var Post_Kind[]
     22         */
     23        private static $kinds = array();
     24
     25        /**
     26         * Whether the built-in kinds have been registered.
     27         *
     28         * @since 4.0.0
     29         * @var bool
     30         */
     31        private static $builtins_loaded = false;
     32
     33        /**
     34         * Loads the built-in kinds and adds the taxonomy's hooks.
     35         *
     36         * @since 2.0.2
     37         */
    1338        public static function init() {
    14                 $wp_version = get_bloginfo( 'version' );
    15                 require_once plugin_dir_path( __FILE__ ) . '/register-kinds.php';
     39                self::load_kinds();
    1640
    1741                // Add the Correct Archive Title to Kind Archives.
    18                 if ( version_compare( $wp_version, '5.5', '>' ) ) {
    19                         add_filter( 'get_the_archive_title', array( self::class, 'kind_archive_title' ), 10 );
    20                 } else {
    21                         add_filter( 'get_the_archive_title', array( self::class, 'kind_archive_title' ), 10, 3 );
    22                 }
     42                add_filter( 'get_the_archive_title', array( self::class, 'kind_archive_title' ), 10 );
    2343
    2444                add_filter( 'get_the_archive_title_prefix', array( self::class, 'kind_archive_prefix' ), 10 );
    … …  
    2646                add_filter( 'document_title_parts', array( self::class, 'document_title_parts' ), 10 );
    2747
     48                // Keep the taxonomy's default term in step with the Default Kind setting.
     49                add_action( 'add_option_kind_default', array( self::class, 'sync_default_term' ) );
     50                add_action( 'update_option_kind_default', array( self::class, 'sync_default_term' ) );
     51
    2852                // Add Kind Permalinks.
    2953                add_filter( 'post_link', array( self::class, 'kind_permalink' ), 10, 3 );
    3054                add_filter( 'post_type_link', array( self::class, 'kind_permalink' ), 10, 3 );
    3155
    32                 // Query Variable to Exclude Kinds from Feed
     56                // Query Variable to Exclude Kinds from Feed.
    3357                add_filter( 'query_vars', array( self::class, 'query_vars' ) );
    3458                add_action( 'pre_get_posts', array( self::class, 'kind_filter_query' ) );
    … …  
    3761                add_action( 'pre_get_posts', array( self::class, 'kind_firehose_query' ), 99 );
    3862
    39                 // Add Dropdown
     63                // Add Dropdown.
    4064                add_action( 'restrict_manage_posts', array( self::class, 'kind_dropdown' ), 10, 2 );
    4165
    … …  
    4367                add_filter( 'webmention_links', array( self::class, 'webmention_links' ), 11, 2 );
    4468
    45                 // Add Links to Enclosures if Appropriate
     69                // Add Links to Enclosures if Appropriate.
    4670                add_filter( 'enclosure_links', array( self::class, 'enclosure_links' ), 11, 2 );
    4771
    … …  
    5175                // Trigger Webmention on Change in Post Status.
    5276                add_filter( 'transition_post_status', array( self::class, 'transition' ), 10, 3 );
    53                 // On Post Save Set Post Format
     77                // On Post Save Set Post Format.
    5478                add_action( 'save_post', array( self::class, 'post_formats' ), 99, 3 );
    5579
    56                 // Create hook triggered by change of kind
     80                // Create hook triggered by change of kind.
    5781                add_action( 'set_object_terms', array( self::class, 'set_object_terms' ), 10, 6 );
    5882
    … …  
    6993        }
    7094
    71         /** Template Redirect
    72          *
     95        /**
     96         * Uses the kind photos template for photo archives, if the theme has one.
     97         *
     98         * @since 3.4.14
     99         *
     100         * @param string $template Path of the template to include.
     101         * @return string Path of the template to include.
    73102         */
    74103        public static function template_include( $template ) {
    75104                global $wp_query;
    76105
    77                 // Only use this template for archive pages
     106                // Only use this template for archive pages.
    78107                if ( is_singular() ) {
    79108                        return $template;
    … …  
    92121        /**
    93122         * Register the Route.
     123         *
     124         * @since 3.3.4
    94125         */
    95126        public static function register_routes() {
    … …  
    107138                                                ),
    108139                                        ),
     140                                        // For editor tools: only users who can write posts need it.
    109141                                        'permission_callback' => function () {
    110                                                 return current_user_can( 'read' );
     142                                                return current_user_can( 'edit_posts' );
    111143                                        },
    112144                                ),
    … …  
    115147        }
    116148
     149        /**
     150         * Returns a kind's registration info for the post-kinds/1.0/fields route.
     151         *
     152         * @since 3.3.4
     153         *
     154         * @param WP_REST_Request $request Request, with the kind slug (or all) in kind.
     155         * @return Post_Kind|Post_Kind[]|WP_Error The kind, all kinds, or an error for an unknown kind.
     156         */
    117157        public static function read( $request ) {
    118158                $kind = $request->get_param( 'kind' );
    119                 return self::get_kind_info( $kind, 'all' );
     159                $info = self::get_kind_info( $kind, 'all' );
     160                if ( false === $info ) {
     161                        return new WP_Error( 'rest_post_kind_not_found', __( 'Unknown post kind.', 'indieweb-post-kinds' ), array( 'status' => 404 ) );
     162                }
     163                return $info;
    120164        }
    121165
    … …  
    123167        /**
    124168         * Add our query variables to query_vars list.
     169         *
     170         * @since 3.2.4
    125171         *
    126172         * @access public
    … …  
    140186         * Filter the query for our post kinds.
    141187         *
    142          * @access public
    143          *
    144          * @param $query
     188         * @since 3.4.14
     189         *
     190         * @access public
     191         *
     192         * @param WP_Query $query The query.
     193         * @return WP_Query The query.
    145194         */
    146195        public static function kind_photo_filter( $query ) {
    147                 // check if the user is requesting an admin page
     196                // check if the user is requesting an admin page.
    148197                if ( is_admin() || ! $query->is_main_query() ) {
    149198                        return $query;
    150199                }
    151200                $photos = get_query_var( 'kind_photos' );
    152                 // Return if  not set
     201                // Return if  not set.
    153202                if ( $photos ) {
    154203                        $query->set( 'posts_per_page', 30 );
    … …  
    156205                        $query->is_comment_feed = false;
    157206                        $query->is_home         = false;
     207                        // Set for posts with images in their content or a photo property; see Kind_Media_Metadata::update_photo_flag().
    158208                        $query->set(
    159209                                'meta_query',
    160210                                array(
    161                                         'relation' => 'OR',
    162211                                        array(
    163                                                 'key'     => '_content_img_ids',
    164                                                 'compare' => 'EXISTS',
    165                                         ),
    166                                         array(
    167                                                 'key'     => 'mf2_photo',
     212                                                'key'     => '_kind_has_photo',
    168213                                                'compare' => 'EXISTS',
    169214                                        ),
    … …  
    177222         * Filter the query for our post kinds.
    178223         *
    179          * @access public
    180          *
    181          * @param $query
     224         * @since 3.2.4
     225         *
     226         * @access public
     227         *
     228         * @param WP_Query $query The query.
     229         * @return WP_Query The query.
    182230         */
    183231        public static function kind_filter_query( $query ) {
    184                 // check if the user is requesting an admin page
     232                // check if the user is requesting an admin page.
    185233                if ( is_admin() || ! $query->is_main_query() ) {
    186234                        return $query;
    187235                }
    188236                $exclude = get_query_var( 'exclude' );
    189                 // Return if both are not set
     237                // Return if both are not set.
    190238                if ( ! taxonomy_exists( $exclude ) ) {
    191239                        return $query;
    … …  
    214262         * Filter the main query for post kinds.
    215263         *
    216          * @access public
    217          *
    218          * @param $query
     264         * @since 3.6.0
     265         *
     266         * @access public
     267         *
     268         * @param WP_Query $query The query.
     269         * @return WP_Query The query.
    219270         */
    220271        public static function kind_firehose_query( $query ) {
    221272
    222                 // check if the user is requesting an admin page
     273                // check if the user is requesting an admin page.
    223274                if ( is_admin() || ! $query->is_main_query() ) {
    224275                        return $query;
    … …  
    246297                }
    247298
    248                 $query->set(
    249                         'tax_query',
     299                $tax_query = array(
    250300                        array(
    251                                 'relation' => 'OR',
    252                                 array(
    253                                         'taxonomy' => 'kind',
    254                                         'field'    => 'slug',
    255                                         'terms'    => $firehose,
    256                                         'operator' => 'IN',
    257                                 ),
    258                                 array(
    259                                         'taxonomy' => 'kind',
    260                                         'operator' => 'NOT EXISTS',
    261                                 ),
    262                         )
    263                 );
     301                                'taxonomy' => 'kind',
     302                                'field'    => 'slug',
     303                                'terms'    => $firehose,
     304                                'operator' => 'IN',
     305                        ),
     306                );
     307                // Posts without a kind are shown too. The upgrade routine gives them one,
     308                // so the costly NOT EXISTS clause is only needed until it completes.
     309                if ( Kind_Upgrade::is_pending() ) {
     310                        $tax_query['relation'] = 'OR';
     311                        $tax_query[]           = array(
     312                                'taxonomy' => 'kind',
     313                                'operator' => 'NOT EXISTS',
     314                        );
     315                }
     316                $query->set( 'tax_query', $tax_query );
    264317                return $query;
    265318        }
    … …  
    268321         * Allows for some pre-defined aliases
    269322         *
    270          * @access public
    271          *
    272          * @param $query
     323         * @since 3.4.19
     324         *
     325         * @access public
     326         *
     327         * @param WP_Query $query The query.
     328         * @return WP_Query The query.
    273329         */
    274330        public static function kind_alias_filter( $query ) {
    … …  
    292348        /**
    293349         * Register our REST API endpoint for post kinds.
     350         *
     351         * @since 3.1.0
    294352         *
    295353         * @access public
    … …  
    300358                        'kind',
    301359                        array(
    302                                 'get_callback'    => array( self::class, 'get_post_kind_slug' ),
     360                                'get_callback'    => array( self::class, 'get_rest_post_kind' ),
    303361                                'update_callback' => array( self::class, 'set_rest_post_kind' ),
    304362                                'schema'          => array(
    305                                         'kind' => __( 'Post Kind', 'indieweb-post-kinds' ),
    306                                         'type' => 'string',
     363                                        'description' => __( 'The post kind, or false if the post has none.', 'indieweb-post-kinds' ),
     364                                        // False is only returned, never accepted: the enum allows kind slugs alone.
     365                                        'type'        => array( 'string', 'boolean' ),
     366                                        'enum'        => self::get_kind_list(),
     367                                        'context'     => array( 'view', 'edit' ),
    307368                                ),
    308369                        )
    … …  
    312373        /**
    313374         * Filter template hierarchy to include our template files.
     375         *
     376         * @since 3.1.2
    314377         *
    315378         * @access public
    … …  
    332395         * Generate a sample permalink for a post.
    333396         *
     397         * @since 3.2.2
     398         *
    334399         * @access public
    335400         *
    … …  
    348413                if ( is_numeric( $permalink[1] ) && $post_id === (int) $permalink[1] ) {
    349414                        $excerpt = self::get_excerpt( $post );
    350                         $excerpt = sanitize_title( mb_strimwidth( wp_strip_all_tags( $excerpt ), 0, 40 ) ); // phpcs:ignore
     415                        $excerpt = sanitize_title( mb_strimwidth( wp_strip_all_tags( $excerpt ), 0, 40 ) );
    351416                        if ( ! empty( $excerpt ) ) {
    352417                                $permalink[1] = wp_unique_post_slug( $excerpt, $post_id, $post->post_status, $post->post_type, $post->post_parent );
    … …  
    359424         * Generate a post title.
    360425         *
    361          * @access public
    362          *
    363          * @param int!WP_Post    $post Post ID or Post Object.
    364          * @return string
     426         * @since 3.3.3
     427         *
     428         * @access public
     429         *
     430         * @param int|WP_Post $post   Post ID or post object.
     431         * @param int         $length Optional. Maximum length of the title. Default 40.
     432         * @return string|null The title, or null if the post does not exist.
    365433         */
    366434        public static function generate_title( $post, $length = 40 ) {
    … …  
    374442                        $kind_post = new Kind_Post( $post );
    375443                        $cite      = $kind_post->get_cite();
    376                         if ( Parse_This_MF2::is_microformat( $cite ) ) {
     444                        // Citations are stored as a list; use the first.
     445                        if ( wp_is_numeric_array( $cite ) && 1 === count( $cite ) ) {
     446                                $cite = $cite[0];
     447                        }
     448                        if ( class_exists( '\\ParseThis\\MF2_Utils' ) && \ParseThis\MF2_Utils::is_microformat( $cite ) ) {
    377449                                if ( array_key_exists( 'name', $cite['properties'] ) ) {
    378450                                        $excerpt = $cite['properties']['name'];
    … …  
    383455                        }
    384456                }
    385                 return mb_strimwidth( $excerpt, 0, $length, '...' ); // phpcs:ignore
     457                return mb_strimwidth( $excerpt, 0, $length, '...' );
    386458        }
    387459
    … …  
    389461         * Filter the post title. Add a symbol at the end if generated title.
    390462         *
    391          * @access public
    392          *
    393          * @param string $title   Current post title
    394          * @param int    $post_id Post ID
     463         * @since 3.0.9
     464         *
     465         * @access public
     466         *
     467         * @param string $title   Current post title.
     468         * @param int    $post_id Post ID.
    395469         * @return string
    396470         */
    … …  
    406480                                $title = sprintf( '[%1$s] %2$s', $post_kind, self::generate_title( $post_id, 60 ) );
    407481                        }
     482                } elseif ( empty( $title ) && is_comment_feed() ) {
     483                        // Comment feed items read "Comment on <title> by <author>"; name untitled posts.
     484                        $title = self::generate_title( $post_id, 60 );
    408485                }
    409486                return $title;
    … …  
    413490         * Filter the single post title.
    414491         *
    415          * @access public
    416          *
    417          * @param string $title   Current post title
    418          * @param WP_Post    $post Post object
     492         * @since 3.3.3
     493         *
     494         * @access public
     495         *
     496         * @param string  $title Current post title.
     497         * @param WP_Post $post  Post object.
    419498         * @return string
    420499         */
    … …  
    438517         * Filter the post excerpt.
    439518         *
     519         * @since 3.2.0
     520         *
    440521         * @access public
    441522         *
    … …  
    456537         * Set our post object terms.
    457538         *
     539         * @since 3.0.8
     540         *
    458541         * @access public
    459542         *
    … …  
    461544         * @param array  $terms      Array of object terms.
    462545         * @param array  $tt_ids     Array of term taxonomy IDs.
    463          * @param string $taxonomy   Taxonomy slug
     546         * @param string $taxonomy   Taxonomy slug.
    464547         * @param bool   $append     If terms should be appended or overwrite.
    465548         * @param array  $old_tt_ids Array of original trem taxonomy IDs.
    … …  
    475558                        $new_term = $new_term instanceof WP_Term ? $new_term->slug : '';
    476559                        if ( $old_term !== $new_term ) {
    477                                 // Trigger a hook on a changed kind identifying old and new so actions can be performed
     560                                /**
     561                                 * Fires when a post's kind changes.
     562                                 *
     563                                 * @since 3.0.8
     564                                 *
     565                                 * @param int    $object_id Post ID.
     566                                 * @param string $old_term  Slug of the previous kind, or an empty string if there was none.
     567                                 * @param string $new_term  Slug of the new kind, or an empty string if the kind was removed.
     568                                 */
    478569                                do_action( 'change_kind', $object_id, $old_term, $new_term );
    479570                        }
    … …  
    483574        /**
    484575         * To Be Run on Plugin Activation.
     576         */
     577        /**
     578         * Registers the built-in kinds, once.
     579         *
     580         * Called from init() and on activation. A plugin is activated after
     581         * plugins_loaded and init have run for the request, so neither the kinds
     582         * nor the register_post_kind() function (kind-functions.php) would
     583         * otherwise be loaded when the kind terms are created.
     584         *
     585         * @since 4.0.0
     586         */
     587        public static function load_kinds() {
     588                if ( self::$builtins_loaded ) {
     589                        return;
     590                }
     591                self::$builtins_loaded = true;
     592                if ( ! function_exists( 'register_post_kind' ) ) {
     593                        require_once plugin_dir_path( __FILE__ ) . 'kind-functions.php';
     594                }
     595                require plugin_dir_path( __FILE__ ) . 'register-kinds.php';
     596        }
     597
     598        /**
     599         * Creates the kind terms on activation.
     600         *
     601         * Stops with an error if the IndieWeb Taxonomy plugin, which Post Kinds
     602         * replaces, is active.
     603         *
     604         * @since 2.0.2
    485605         */
    486606        public static function activate_kinds() {
    487607                if ( function_exists( 'iwt_plugin_notice' ) ) {
    488                         deactivate_plugins( plugin_basename( __FILE__ ) );
    489                         wp_die( 'You have Indieweb Taxonomy activated. Post Kinds replaces this plugin. Please disable Taxonomy before activating' );
    490                 }
     608                        deactivate_plugins( plugin_basename( dirname( __DIR__ ) . '/indieweb-post-kinds.php' ) );
     609                        wp_die( esc_html__( 'Post Kinds replaces the IndieWeb Taxonomy plugin. Deactivate IndieWeb Taxonomy, then activate Post Kinds.', 'indieweb-post-kinds' ) );
     610                }
     611                self::load_kinds();
    491612                self::register();
    492613                self::kind_defaultterms();
    … …  
    494615        }
    495616
     617        /**
     618         * Returns the rewrite regex for the pagination part of a URL.
     619         *
     620         * @since 3.5.0
     621         *
     622         * @return string The regex, such as 'page/?([0-9]{1,})'.
     623         */
    496624        public static function get_pagination_regex() {
    497625                global $wp_rewrite;
    … …  
    499627        }
    500628
     629        /**
     630         * Returns the rewrite regex for the feed part of a URL.
     631         *
     632         * @since 3.5.0
     633         *
     634         * @param bool $with_base Optional. Whether to start with the feed base ('feed/'). Default true.
     635         * @return string The regex, such as 'feed/(feed|rdf|rss|rss2|atom)'.
     636         */
    501637        public static function get_feed_regex( $with_base = true ) {
    502638                global $wp_rewrite;
    503                 // Build a regex to match the feed section of URLs, something like (feed|atom|rss|rss2)/?
     639                // Build a regex to match the feed section of URLs, something like (feed|atom|rss|rss2)/?.
    504640                $feedregex2 = '';
    505641                foreach ( (array) $wp_rewrite->feeds as $feed_name ) {
    … …  
    511647        }
    512648
     649        /**
     650         * Joins regex parts into a rewrite rule pattern.
     651         *
     652         * @since 3.5.0
     653         *
     654         * @param string[] $elements Parts of the pattern, in order.
     655         * @return string The parts joined by slashes and ending in '?$', or an empty string if there are none.
     656         */
    513657        public static function generate_permastruct( $elements ) {
    514658                if ( empty( $elements ) || ! is_array( $elements ) ) {
    … …  
    520664
    521665        /**
     666         * Returns the start of kind archive URLs, as in /kind/like/.
     667         *
     668         * Set as the Kind base on Settings > Permalinks.
     669         *
     670         * @since 4.0.0
     671         *
     672         * @return string The base. Default 'kind'.
     673         */
     674        public static function get_kind_base() {
     675                $base = get_option( 'kind_base', '' );
     676                $base = is_string( $base ) && '' !== $base ? $base : 'kind';
     677                /**
     678                 * Filters the start of kind archive URLs, as in /kind/like/.
     679                 *
     680                 * The Kind base on Settings > Permalinks sets it. Flush rewrite rules after
     681                 * changing this.
     682                 *
     683                 * @since 4.0.0
     684                 *
     685                 * @param string $base The base. Default the Kind base setting, or 'kind'.
     686                 */
     687                return (string) apply_filters( 'kind_base_slug', $base );
     688        }
     689
     690        /**
    522691         * Register the custom taxonomy for kinds.
     692         *
     693         * @since 2.0.2
    523694         */
    524695        public static function register() {
    … …  
    540711                        'add_or_remove_items'        => _x( 'Add or remove kinds', 'add or remove items', 'indieweb-post-kinds' ),
    541712                        'choose_from_most_used'      => _x( 'Choose from the most used kinds', 'choose most used', 'indieweb-post-kinds' ),
    542                         'not found'                  => _x( 'No kinds found', 'no kinds found', 'indieweb-post-kinds' ),
     713                        'not_found'                  => _x( 'No kinds found', 'no kinds found', 'indieweb-post-kinds' ),
    543714                        'no_terms'                   => _x( 'No kinds', 'no kinds', 'indieweb-post-kinds' ),
    544715                        'back_to_items'              => __( 'Back to Kinds', 'indieweb-post-kinds' ),
    … …  
    546717                        'item_link_description'      => __( 'A link to a kind', 'indieweb-post-kinds' ),
    547718                );
     719
     720                // The slug is used as the name too: core finds the default term by name, and a translated name would not match.
     721                $default_kind = get_option( 'kind_default', 'note' );
    548722
    549723                $args = array(
    … …  
    560734                        'show_admin_column'  => true,
    561735                        'meta_box_cb'        => array( self::class, 'select_metabox' ),
    562                         'rewrite'            => true,
     736                        'rewrite'            => array( 'slug' => self::get_kind_base() ),
    563737                        'query_var'          => true,
    564738                        'default_term'       => array(
    565                                 'name' => __( 'Article', 'indieweb-post-kinds' ),
    566                                 'slug' => 'article',
     739                                'name' => $default_kind,
     740                                'slug' => $default_kind,
    567741                        ),
    568742
    569743                );
    570744                register_taxonomy( 'kind', array( 'post' ), $args );
    571                 add_permastruct( 'kind_date', 'kind/%kind%/%year%/%monthnum%/' );
    572 
     745                add_permastruct( 'kind_date', self::get_kind_base() . '/%kind%/%year%/%monthnum%/' );
     746
     747                /**
     748                 * Filters the taxonomies that get kind archive rewrite rules.
     749                 *
     750                 * Each taxonomy gets rules for /kind/{kind}/{taxonomy}/{term}/, where kind is the Kind base. Rewrite rules
     751                 * are flushed when the plugin is activated or updated; flush them yourself
     752                 * (Settings > Permalinks > Save) after changing this.
     753                 *
     754                 * @since 3.4.15
     755                 *
     756                 * @param string[] $taxonomies Taxonomy names. Default 'post_tag', 'category' and 'series'.
     757                 */
    573758                $kind_rewrite_taxonomies = apply_filters( 'kind_rewrite_taxonomies', array( 'post_tag', 'category', 'series' ) );
    574759
    575760                // For each kind, support filtering by other taxonomies.
    576761                foreach ( $kind_rewrite_taxonomies as $taxonomy ) {
    577                         add_permastruct( 'kind_' . $taxonomy, 'kind/%kind%/' . $taxonomy . '/%' . $taxonomy . '%' );
    578                 }
    579 
     762                        add_permastruct( 'kind_' . $taxonomy, self::get_kind_base() . '/%kind%/' . $taxonomy . '/%' . $taxonomy . '%' );
     763                }
     764
     765                /**
     766                 * Filters the URL slug for archives that exclude kinds, as in /exclude/kind/{kinds}/.
     767                 *
     768                 * Flush rewrite rules after changing this.
     769                 *
     770                 * @since 3.4.15
     771                 *
     772                 * @param string $slug Slug. Default 'exclude'.
     773                 */
    580774                $kind_exclude_slug = apply_filters( 'kind_exclude_slug', 'exclude' );
    581775
    … …  
    584778                add_permastruct( 'kind_excludes', $kind_exclude_slug . '/%kind_exclude%/%kind_exclude_terms%' );
    585779
     780                /**
     781                 * Filters the URL slug for the archive of posts with photos.
     782                 *
     783                 * Flush rewrite rules after changing this.
     784                 *
     785                 * @since 3.4.15
     786                 *
     787                 * @param string $slug Slug. Default 'photos'.
     788                 */
    586789                $kind_photos_slug   = apply_filters( 'kind_photos_slug', 'photos' );
     790                /**
     791                 * Filters the URL slug for the firehose feed, which includes every kind.
     792                 *
     793                 * Flush rewrite rules after changing this.
     794                 *
     795                 * @since 3.6.0
     796                 *
     797                 * @param string $slug Slug. Default 'firehose'.
     798                 */
    587799                $kind_firehose_slug = apply_filters( 'kind_firehose_slug', 'firehose' );
    588800
    589801                $year_regex       = '([0-9]{4})';
    590                 $month_regex      = '(0-9]{2})';
     802                $month_regex      = '([0-9]{2})';
    591803                $day_regex        = '([0-9]{1,})';
    592804                $pagination_regex = self::get_pagination_regex();
    593805                $feed_regex       = self::get_feed_regex();
    594806                $feed_regex2      = self::get_feed_regex( false );
    595                 $tax_regex        = '([^/]*)';
    596                 $term_regex       = '([^/]*)';
    597 
    598                 // Year Archives for Photos
     807
     808                // Year Archives for Photos.
    599809                add_rewrite_rule(
    600810                        self::generate_permastruct( array( $kind_photos_slug, $year_regex, $pagination_regex ) ),
    … …  
    608818                );
    609819
    610                 // Year and Month Archive for Photos
     820                // Year and Month Archive for Photos.
    611821                add_rewrite_rule(
    612822                        self::generate_permastruct( array( $kind_photos_slug, $year_regex, $month_regex, $pagination_regex ) ),
    … …  
    620830                );
    621831
    622                 // Year and Month And Day Archive for Photos
     832                // Year and Month And Day Archive for Photos.
    623833                add_rewrite_rule(
    624834                        self::generate_permastruct( array( $kind_photos_slug, $year_regex, $month_regex, $day_regex, $pagination_regex ) ),
    … …  
    632842                );
    633843
    634                 // Month And Day Archive for Photos
     844                // Month And Day Archive for Photos.
    635845                add_rewrite_rule(
    636846                        self::generate_permastruct( array( $kind_photos_slug, $month_regex, $day_regex, $pagination_regex ) ),
    … …  
    698908
    699909        /**
     910         * Points the kind taxonomy's default term at the Default Kind setting.
     911         *
     912         * Core sets the default term when the taxonomy is registered; this applies a
     913         * change to the setting within the same request.
     914         *
     915         * @since 4.0.0
     916         */
     917        public static function sync_default_term() {
     918                $term = get_term_by( 'slug', get_option( 'kind_default', 'note' ), 'kind' );
     919                if ( $term ) {
     920                        update_option( 'default_term_kind', $term->term_id );
     921                }
     922        }
     923
     924        /**
    700925         * Sets up Default Terms for Kind Taxonomy.
     926         *
     927         * Creates a term for each registered kind that has none, and updates the
     928         * name and description of terms that differ from the kind's, for example
     929         * after a translation changes. Terms that already match are not written.
     930         *
     931         * @since 2.0.2
    701932         */
    702933        public static function kind_defaultterms() {
    703                 $terms = self::get_kind_list();
    704                 foreach ( $terms as $term ) {
    705                         if ( ! get_term_by( 'slug', $term, 'kind' ) ) {
     934                $existing = get_terms(
     935                        array(
     936                                'taxonomy'   => 'kind',
     937                                'hide_empty' => false,
     938                        )
     939                );
     940                $existing = is_wp_error( $existing ) ? array() : array_column( $existing, null, 'slug' );
     941                foreach ( self::get_kind_list() as $term ) {
     942                        if ( ! isset( $existing[ $term ] ) ) {
    706943                                self::create_post_kind( $term );
    707                         } else {
     944                                continue;
     945                        }
     946                        $kind = self::get_post_kind_info( $term );
     947                        if ( $kind && ( $existing[ $term ]->name !== $kind->singular_name || $existing[ $term ]->description !== $kind->description ) ) {
    708948                                self::update_post_kind( $term );
    709949                        }
    … …  
    714954         * Update our post kind if the term already exists.
    715955         *
     956         * @since 3.1.0
     957         *
    716958         * @access private
    717959         *
    718          * @param $term
     960         * @param string $term Post kind slug.
    719961         */
    720962        private static function update_post_kind( $term ) {
    … …  
    740982         * Create our post kind term.
    741983         *
     984         * @since 3.1.0
     985         *
    742986         * @access private
    743987         *
    744          * @param $term
     988         * @param string $term Post kind slug.
    745989         */
    746990        private static function create_post_kind( $term ) {
    747991                $kind = self::get_post_kind_info( $term );
    748992                if ( $kind ) {
     993                        // The first argument is the term name; wp_insert_term() has no 'name' argument.
    749994                        wp_insert_term(
    750                                 $kind->slug,
     995                                $kind->singular_name,
    751996                                'kind',
    752997                                array(
    753                                         'name'        => $kind->singular_name,
    754998                                        'description' => $kind->description,
    755999                                        'slug'        => $term,
    … …  
    7621006         * Filter our post kind permalink.
    7631007         *
     1008         * @since 2.0.2
     1009         *
    7641010         * @access public
    7651011         *
    … …  
    7731019                        return $permalink; }
    7741020
    775                 // Get post
     1021                // Get post.
    7761022                $post = get_post( $post_id );
    7771023                if ( ! $post ) {
    7781024                        return $permalink; }
    7791025
    780                 // Get taxonomy terms
    781                 $terms = wp_get_object_terms( $post->ID, 'kind' );
    782                 if ( ! is_wp_error( $terms ) && ! empty( $terms ) && is_object( $terms[0] ) ) {
     1026                // Get taxonomy terms; get_the_terms() uses the term cache the main query primes.
     1027                $terms = get_the_terms( $post, 'kind' );
     1028                if ( is_array( $terms ) && ! empty( $terms ) && is_object( $terms[0] ) ) {
    7831029                        $taxonomy_slug = $terms[0]->slug;
    7841030                } else {
    … …  
    7891035        /**
    7901036         * Fetch the current post kind terms from the current query.
     1037         *
     1038         * @since 3.0.3
    7911039         *
    7921040         * @access public
    … …  
    8121060         * Prefix Introduced in WP5.5
    8131061         *
     1062         * @since 3.5.0
     1063         *
    8141064         * @access public
    8151065         *
    … …  
    8311081         * Original Title and Prefix introduced in WP5.5.
    8321082         *
     1083         * @since 2.0.2
     1084         *
    8331085         * @access public
    8341086         *
    … …  
    8391091         */
    8401092        public static function kind_archive_title( $title, $original_title = null, $prefix = null ) {
    841                 $return     = array();
    842                 $wp_version = get_bloginfo( 'version' );
    843 
     1093                $return = array();
     1094
     1095                /**
     1096                 * This filter is documented in wp-includes/general-template.php
     1097                 */
    8441098                $prefix = apply_filters( 'get_the_archive_title_prefix', $prefix );
    8451099
    … …  
    8841138        /**
    8851139         * Filters the post kind archive description.
     1140         *
     1141         * @since 2.5.2
    8861142         *
    8871143         * @access public
    … …  
    9111167         * Sets Post Format for Post Kind.
    9121168         *
    913          * @param int     $post_id Post ID
    914          * @param WP_Post $post Post Object
    915          * @param boolean $update,
     1169         * @since 2.5.0
     1170         *
     1171         * @param int     $post_id Post ID.
     1172         * @param WP_Post $post    Post object.
     1173         * @param bool    $update  Whether this is an existing post being updated.
    9161174         */
    9171175        public static function post_formats( $post_id, $post, $update ) {
    … …  
    9251183         * Fiters the `<title>` tag parts for the post kind.
    9261184         *
     1185         * @since 3.1.3
     1186         *
    9271187         * @access public
    9281188         *
    … …  
    9321192        public static function document_title_parts( $parts ) {
    9331193                if ( is_tax() || is_category() || is_tag() ) {
    934                         $terms = self::get_terms_from_query();
     1194                        $terms  = self::get_terms_from_query();
     1195                        $return = array();
    9351196                        foreach ( $terms as $term ) {
    9361197                                if ( 'kind' === $term->taxonomy ) {
    … …  
    9501211         * Callback for the taxonomy meta box for our post kinds taxonomy.
    9511212         *
     1213         * @since 2.0.2
     1214         *
    9521215         * @access public
    9531216         *
    … …  
    9551218         */
    9561219        public static function select_metabox( $post ) {
    957                 $include = get_option( 'kind_termslist' );
    958                 $include = array_merge( $include, array( 'note', 'reply', 'article' ) );
    959                 // If Simple Location is Enabled, include the check-in type
    960                 // Filter Kinds
     1220                $include = array_merge( Kind_Config::get_termslist(), array( 'note', 'reply', 'article' ) );
     1221                /**
     1222                 * Filters the kinds offered in the editor and listed by the Kind Menu widget.
     1223                 *
     1224                 * By default these are the kinds selected in the settings, plus note, reply and article.
     1225                 *
     1226                 * @since 2.0.2
     1227                 *
     1228                 * @param string[] $include Kind slugs.
     1229                 */
    9611230                $include = array_unique( apply_filters( 'kind_include', $include ) );
    962                 // Note cannot be removed or disabled without hacking the code
     1231                // Note cannot be removed or disabled without hacking the code.
    9631232                if ( ! in_array( 'note', $include, true ) ) {
    9641233                        $include[] = 'note';
    9651234                }
    966                 if ( isset( $_GET['kind'] ) ) {
    967                         $default = get_term_by( 'slug', $_GET['kind'], 'kind' );
     1235                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Preselects the kind on the new post screen; nothing is saved.
     1236                $requested = ( isset( $_GET['kind'] ) && is_string( $_GET['kind'] ) ) ? sanitize_key( wp_unslash( $_GET['kind'] ) ) : '';
     1237                if ( '' !== $requested ) {
     1238                        $default = get_term_by( 'slug', $requested, 'kind' );
    9681239                } elseif ( 'publish' === get_post_status( $post ) ) {
    969                         // On existing published posts without a kind fall back on article which most closely mimics the behavior of an unclassified post
     1240                        // On existing published posts without a kind fall back on article which most closely mimics the behavior of an unclassified post.
    9701241                        $default = get_term_by( 'slug', 'article', 'kind' );
    9711242                } else {
    … …  
    10031274         * Register our post kinds.
    10041275         *
     1276         * @since 3.1.0
     1277         *
    10051278         * @access public
    10061279         *
    10071280         * @param string $slug Post kind slug.
    1008          * @param array  $args Post kind arguments {
    1009                  * @param string $singular_name  Name for one instance of the kind.
    1010                  * @param string $name   General name for the kind plural.
    1011                  * @param string $verb The string for the verb or action (liked this).
    1012                  * @param string $property Microformats 2 property, superseded by properties
    1013                  * @param array $properties Properties is an array outlining the fields for the particular kind. See Kind Fields class for more details
    1014                  * @param string $format Post Format that maps to this.
    1015                  * @param string $description Description of the Kind
    1016                  * @param url $description-url Link to more information
    1017                  * @param string $title Should this kind have an explicit title.
    1018                  * @param boolean $show Show in Settings.
     1281         * @param array  $args {
     1282         *     Post kind arguments. Arguments that are not listed are kept and can
     1283         *     be read as properties of the Post_Kind.
     1284         *
     1285         *     @type string $singular_name   Name for one instance of the kind. Default the slug.
     1286         *     @type string $name            General name for the kind, plural. Default the slug.
     1287         *     @type string $verb            The verb or action, such as 'Liked'. Default the slug.
     1288         *     @type string $property        Microformats 2 property, such as 'like-of'. Default empty.
     1289         *     @type array  $properties      The fields for the kind. See Kind_Fields.
     1290         *     @type string $format          Post format that maps to this kind. Default 'standard'.
     1291         *     @type string $icon            Icon name. Default the slug.
     1292         *     @type string $description     Description of the kind.
     1293         *     @type string $description_url Link to more information.
     1294         *     @type string $shortlink       Shortlink code for the kind.
     1295         *     @type bool   $title           Whether the kind should have an explicit title.
     1296         *     @type bool   $show            Whether to show the kind in Settings. Default false.
    10191297         * }
    1020          * @return bool
     1298         * @return bool True if the kind was registered, false if it already exists.
    10211299         */
    10221300        public static function register_post_kind( $slug, $args ) {
    10231301                $kind = new Post_Kind( $slug, $args );
    1024                 // Do not allow reregistering existing kinds
     1302                // Do not allow reregistering existing kinds.
    10251303                if ( isset( self::$kinds[ $slug ] ) ) {
    10261304                        return false;
    … …  
    10321310        /**
    10331311         * Retrieve info on a provided post kind.
     1312         *
     1313         * @since 3.1.0
    10341314         *
    10351315         * @access public
    … …  
    10451325        }
    10461326
    1047         // Enable a hidden post kind
    1048 
    10491327        /**
    10501328         * Enables a hidden post kind.
     1329         *
     1330         * @since 3.1.0
    10511331         *
    10521332         * @access public
    … …  
    10671347         * Retrieve list of registered post kinds.
    10681348         *
     1349         * @since 3.1.0
     1350         *
    10691351         * @access public
    10701352         * @return array
    … …  
    10771359         * Returns all translated strings.
    10781360         *
     1361         * @since 2.5.2
     1362         *
    10791363         * @param string $kind     Post Kind to return.
    1080          * @param string $property The individual property
     1364         * @param string $property The individual property.
    10811365         * @return string|array Return kind-property. If either is set to all, return all.
    10821366         */
    … …  
    10951379                        return $k;
    10961380                }
    1097                 if ( ! array_key_exists( $property, get_object_vars( $k ) ) ) {
     1381                if ( ! $k->has( $property ) ) {
    10981382                        return false;
    10991383                }
    … …  
    11031387        /**
    11041388         * Add available webmention links.
     1389         *
     1390         * @since 2.3.5
    11051391         *
    11061392         * @access public
    … …  
    11251411        /**
    11261412         * Add available enclosure links.
     1413         *
     1414         * @since 3.0.4
    11271415         *
    11281416         * @access public
    … …  
    11501438         * Generate a dropdown of our post kinds for the post list table view.
    11511439         *
     1440         * @since 2.5.2
     1441         *
    11521442         * @access public
    11531443         * @param string $post_type Current post type being listed.
    … …  
    11571447                if ( 'post' === $post_type ) {
    11581448                        $taxonomy      = 'kind';
    1159                         $selected      = isset( $_GET[ $taxonomy ] ) ? $_GET[ $taxonomy ] : '';
     1449                        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- A list-table filter; nothing is saved.
     1450                        $selected      = ( isset( $_GET[ $taxonomy ] ) && is_string( $_GET[ $taxonomy ] ) ) ? sanitize_key( wp_unslash( $_GET[ $taxonomy ] ) ) : '';
    11601451                        $kind_taxonomy = get_taxonomy( $taxonomy );
    11611452                        wp_dropdown_categories(
    … …  
    11791470         * Set our post kind terms upon publish transition status.
    11801471         *
    1181          * @access public
    1182          * @param int          $post_id Current post ID
     1472         * @since 2.0.2
     1473         *
     1474         * @access public
     1475         *
     1476         * @param int          $post_id Current post ID.
    11831477         * @param WP_Post|null $post    Current post object.
    11841478         */
    … …  
    11871481                        return;
    11881482                }
    1189                 $count = wp_get_post_terms( $post_id, 'kind' );
    1190                 if ( is_countable( $count ) && $count <= 0 ) {
     1483                $terms = wp_get_post_terms( $post_id, 'kind' );
     1484                if ( ! is_wp_error( $terms ) && empty( $terms ) ) {
    11911485                        set_post_kind( $post_id, get_option( 'kind_default' ) );
    11921486                }
    … …  
    11951489        /**
    11961490         * Maybe set our post kind terms upon post status transition.
     1491         *
     1492         * @since 2.0.2
    11971493         *
    11981494         * @access public
    … …  
    12111507         * Filter the classes on a post's markup.
    12121508         *
     1509         * @since 2.0.2
     1510         *
    12131511         * @access public
    12141512         *
    … …  
    12271525         * Returns a pretty, translated version of a post kind slug.
    12281526         *
     1527         * @since 2.1.1
     1528         *
    12291529         * @param string $slug A post format slug.
    12301530         * @return string The translated post format name.
    … …  
    12321532        public static function get_post_kind_string( $slug ) {
    12331533                $string = self::get_kind_info( $slug, 'singular_name' );
    1234                 return $slug ? $slug : '';
     1534                return $string ? $string : '';
    12351535        }
    12361536
    12371537        /**
    12381538         * Returns a link to a post kind index.
     1539         *
     1540         * @since 2.1.1
    12391541         *
    12401542         * @param string $kind The post kind slug.
    … …  
    12521554         * Retrieve a total count statistic for a given post kind.
    12531555         *
     1556         * @since 3.1.3
     1557         *
    12541558         * @access public
    12551559         *
    … …  
    12671571        /**
    12681572         * Returns the post kind slug for the current post.
     1573         *
     1574         * @since 2.1.1
    12691575         *
    12701576         * @access public
    … …  
    12881594         * Returns the post kind name for the current post.
    12891595         *
    1290          * @access public
    1291          *
    1292          * @param WP_Post|int|null $post
    1293          * @return array|bool|string
     1596         * @since 2.1.1
     1597         *
     1598         * @access public
     1599         *
     1600         * @param WP_Post|int|null $post Optional. Post ID or post object. Default the current post.
     1601         * @return string|false The kind's translated name, or false if the post has no kind.
    12941602         */
    12951603        public static function get_post_kind( $post = null ) {
    … …  
    13061614         * Check if a post has any of the given kinds, or any kind.
    13071615         *
     1616         * @since 2.1.1
     1617         *
    13081618         * @uses has_term()
    13091619         *
    … …  
    13251635         * Assign a kind to a post.
    13261636         *
     1637         * @since 2.1.1
     1638         *
    13271639         * @param int|WP_Post $post The post for which to assign a kind.
    1328          * @param string     $kind A kind to assign. Using an empty string or array will default to article.
    1329          * @return mixed WP_Error on error. Array of affected term IDs on success.
     1640         * @param string      $kind Optional. A registered kind's slug. Default 'article'.
     1641         * @return array|false|WP_Error Array of affected term IDs on success. WP_Error if the post
     1642         *                              or the kind is not valid, or false on failure.
    13301643         */
    13311644        public static function set_post_kind( $post, $kind = 'article' ) {
    … …  
    13411654
    13421655        /**
    1343          * Update callback for REST API endpoint.
    1344          *
    1345          * @access public
    1346          *
    1347          * @param string $kind Post kind being processed.
    1348          * @param $post_array
    1349          */
    1350         public static function set_rest_post_kind( $kind, $post_array ) {
    1351                 if ( isset( $post_array['id'] ) ) {
    1352                         self::set_post_kind( $post_array['id'], $kind );
    1353                 }
     1656         * Update callback for the kind REST field.
     1657         *
     1658         * @since 3.1.0
     1659         *
     1660         * @access public
     1661         *
     1662         * @param string        $kind Post kind to set.
     1663         * @param WP_Post|array $post Post being updated.
     1664         * @return true|WP_Error True on success, WP_Error if the kind could not be set.
     1665         */
     1666        public static function set_rest_post_kind( $kind, $post ) {
     1667                // WordPress passes the post being updated as a WP_Post object.
     1668                $post_id = $post instanceof WP_Post ? $post->ID : ( is_array( $post ) && isset( $post['id'] ) ? (int) $post['id'] : 0 );
     1669                if ( ! $post_id ) {
     1670                        return new WP_Error( 'rest_invalid_post', __( 'Invalid post.', 'indieweb-post-kinds' ), array( 'status' => 400 ) );
     1671                }
     1672                if ( ! current_user_can( 'edit_post', $post_id ) ) {
     1673                        return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to set the kind of this post.', 'indieweb-post-kinds' ), array( 'status' => rest_authorization_required_code() ) );
     1674                }
     1675                $result = self::set_post_kind( $post_id, $kind );
     1676                if ( is_wp_error( $result ) ) {
     1677                        $result->add_data( array( 'status' => 400 ) );
     1678                        return $result;
     1679                }
     1680                return true;
     1681        }
     1682
     1683        /**
     1684         * Get callback for the kind REST field.
     1685         *
     1686         * @since 4.0.0
     1687         *
     1688         * @access public
     1689         *
     1690         * @param array $post Prepared post data, as an array with an id key.
     1691         * @return string|false The post's kind slug, or false if it has none.
     1692         */
     1693        public static function get_rest_post_kind( $post ) {
     1694                // The prepared post is an array; passing it to get_post() would cast it to 1.
     1695                $post_id = is_array( $post ) && isset( $post['id'] ) ? (int) $post['id'] : 0;
     1696                return $post_id ? self::get_post_kind_slug( $post_id ) : false;
    13541697        }
    13551698
    … …  
    13571700         * Whether or not to display the before kind content.
    13581701         *
     1702         * @since 2.6.6
     1703         *
    13591704         * @access public
    13601705         * @return mixed|void
    13611706         */
    13621707        public static function before_kind() {
     1708                /**
     1709                 * Filters whether to show the kind's icon or name before the response.
     1710                 *
     1711                 * Return false to hide it everywhere and remove the display option from the settings page.
     1712                 *
     1713                 * @since 2.6.6
     1714                 *
     1715                 * @param bool $display Whether to show it. Default true.
     1716                 */
    13631717                return apply_filters( 'kind_icon_display', true );
    13641718        }
    … …  
    13671721         * Display before Kind - either icon text or no display.
    13681722         *
     1723         * @since 2.6.6
     1724         *
    13691725         * @param string $kind    The slug for the kind of the current post.
    1370          * @param string $display Override display;
     1726         * @param string $display Override display.
    13711727         * @return string Marked up kind information.
    13721728         */
    … …  
    13781734                        $display = get_option( 'kind_display' );
    13791735                }
    1380                 $text = '<span class="kind-display-text">' . self::get_kind_info( $kind, 'verb' ) . '</span> ';
     1736                $text = '<span class="kind-display-text">' . esc_html( self::get_kind_info( $kind, 'verb' ) ) . '</span> ';
    13811737                $icon = self::get_icon( $kind );
    1382                 // Hide Icon in Feed View
     1738                // Hide Icon in Feed View.
    13831739                if ( 'text' !== $display && is_feed() ) {
    13841740                        $icon = '';
    … …  
    13991755         * Retrieve the icon SVG for a provided post kind.
    14001756         *
     1757         * @since 2.4.0
     1758         *
    14011759         * @access public
    14021760         *
    … …  
    14061764         */
    14071765        public static function get_icon( $kind, $echo = false ) {
     1766                // Icons are shown per post, per widget item and per settings row; read each file once.
     1767                static $svgs = array();
     1768                $kind        = sanitize_key( $kind );
     1769                if ( ! isset( $svgs[ $kind ] ) ) {
     1770                        $svg           = sprintf( '%1$ssvgs/%2$s.svg', plugin_dir_path( __DIR__ ), $kind );
     1771                        $svgs[ $kind ] = file_exists( $svg ) ? (string) file_get_contents( $svg ) : '';
     1772                        // A default size for places without the plugin's stylesheet, such as feed readers. CSS overrides it.
     1773                        $svgs[ $kind ] = preg_replace( '/^<svg\b/', '<svg width="1em" height="1em"', $svgs[ $kind ] );
     1774                }
     1775                if ( '' === $svgs[ $kind ] ) {
     1776                        return '';
     1777                }
    14081778                $name       = self::get_kind_info( $kind, 'singular_name' );
    1409                 $svg        = sprintf( '%1$ssvgs/%2$s.svg', plugin_dir_path( __DIR__ ), $kind );
    1410                 $attributes = apply_filters( 'post_kinds_icon_attributes', 'style="display: inline-block; max-height: 1rem; margin-right: 0.5rem"' );
    1411                 if ( file_exists( $svg ) ) {
    1412                         $return = sprintf( '<span class="svg-icon svg-%1$s" aria-label="%2$s" title="%2$s" %3$s><span aria-hidden="true">%4$s</span></span>', esc_attr( $kind ), esc_attr( $name ), $attributes, file_get_contents( $svg ) );
    1413                 } else {
    1414                         return '';
    1415                 }
     1779                /**
     1780                 * Filters the HTML attributes added to the span that wraps a kind icon.
     1781                 *
     1782                 * The string is output as-is, so it must be safe, escaped markup.
     1783                 *
     1784                 * @since 3.5.8
     1785                 * @since 4.0.0 The default is empty; the stylesheet sizes the icon.
     1786                 *
     1787                 * @param string $attributes HTML attributes. Default empty.
     1788                 */
     1789                $attributes = (string) apply_filters( 'post_kinds_icon_attributes', '' );
     1790                $attributes = '' === $attributes ? '' : ' ' . $attributes;
     1791                $return     = sprintf( '<span class="svg-icon svg-%1$s" aria-label="%2$s" title="%2$s"%3$s><span aria-hidden="true">%4$s</span></span>', esc_attr( $kind ), esc_attr( $name ), $attributes, $svgs[ $kind ] );
    14161792                if ( $echo ) {
    1417                         echo $return; // phpcs:ignore
     1793                        echo $return; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The plugin's own SVG icon; the kind and name are escaped, and the attributes come from the post_kinds_icon_attributes filter.
    14181794                }
    14191795                return $return;
  • indieweb-post-kinds/trunk/includes/class-kind-view.php

    r3001401 r3738687  
    11<?php
    22/**
    3  * Post Kind View Class
     3 * Kind_View class.
    44 *
    5  * Includes Helper Functions to Set Up Display Behavior and Allows Calling of View Templates
     5 * @package Post_Kinds
     6 * @since   2.0.2
    67 */
    78
    8 // The Kind_View class sets up the kind display behavior for kinds
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Sets up how kinds are displayed, and locates and renders the view templates.
     13 *
     14 * @since 2.0.2
     15 */
    916class Kind_View {
     17        /**
     18         * Adds the hooks that display kind markup.
     19         *
     20         * @since 2.0.2
     21         */
    1022        public static function init() {
    1123
     24                /**
     25                 * Filters whether to add response markup to post content, excerpts and feeds automatically.
     26                 *
     27                 * Return false to place it in the theme instead, for example with
     28                 * Kind_View::get_display().
     29                 *
     30                 * @since 3.6.0
     31                 *
     32                 * @param bool $display Whether to add it. Default true.
     33                 */
    1234                if ( apply_filters( 'kind_content_display', true ) ) {
    1335                        add_filter( 'the_content', array( static::class, 'content_response' ), 9 );
    … …  
    2244        /**
    2345         * Filters the attachment image attributes for image post kinds.
     46         *
     47         * @since 2.6.1
    2448         *
    2549         * @access public
    … …  
    5579        }
    5680
    57         /*
    58          * Function will locate the correct template and return it.
    59          *
    60          * @param string $slug Post Kind Slug.
    61          * @param string $name Post Kind Term Name.
    62          * @return string|null File if located or null if unable to find.
     81        /**
     82         * Finds the view file for a kind, in the theme or the plugin.
     83         *
     84         * @since 3.4.0
     85         *
     86         * @param string $slug View slug, such as 'kind'.
     87         * @param string $name Kind slug, such as 'like'.
     88         * @return string|null Path of the view file, an empty string if no kind is given,
     89         *                     or null if no view file exists.
    6390         */
    6491        public static function locate_view( $slug, $name ) {
    … …  
    6794                        return '';
    6895                }
    69                 $templates[] = "{$slug}-{$name}.php";
    70                 $templates[] = "{$slug}.php";
     96                $templates   = array( "{$slug}-{$name}.php", "{$slug}.php" );
     97                /**
     98                 * Filters the folders searched for view files before the plugin's own views folder.
     99                 *
     100                 * @since 3.3.0
     101                 *
     102                 * @param string[] $paths Folder paths, each with a trailing slash. Default the theme's kind_views folder.
     103                 */
    71104                $look        = apply_filters( 'kind_view_paths', array( get_theme_file_path( 'kind_views/' ) ) );
    72105                $look[]      = plugin_dir_path( __DIR__ ) . 'views/';
    … …  
    94127         * Function will return the output.
    95128         *
     129         * @since 2.4.0
     130         *
    96131         * @access public
    97132         *
    98133         * @param string $slug Post kind slug.
    99134         * @param string $name Post kind term name.
    100          * @param array $args Optional Arguments
     135         * @param array  $args Optional. Arguments for the view. 'post_id' is the post's ID.
    101136         * @return string
    102137         */
    … …  
    124159                }
    125160
    126                 $author = self::get_hcard( $cite['author'] );
     161                // Every author's h-card, as a list such as "Ann, Bob and Cat".
     162                $author = array_filter( array_map( array( static::class, 'get_hcard' ), $cite['authors'] ) );
     163                $author = empty( $author ) ? false : wp_sprintf( '%l', $author );
    127164
    128165                $url   = $cite['url'];
    … …  
    140177         * Return the post kind display.
    141178         *
     179         * @since 2.1.1
     180         *
    142181         * @access public
    143182         *
    … …  
    151190                if ( 'post' === get_post_type( $post_id ) ) {
    152191                        $kind    = get_post_kind_slug( $post_id );
    153                         $content = self::get_view_part( 'kind', $kind );
     192                        $content = self::get_view_part( 'kind', $kind, array( 'post_id' => $post_id ) );
     193                        /**
     194                         * Filters the response markup for a post.
     195                         *
     196                         * @since 2.0.2
     197                         *
     198                         * @param string $content Rendered view markup.
     199                         * @param int    $post_id Post ID.
     200                         */
    154201                        return apply_filters( 'kind_response_display', $content, $post_id );
    155202                }
    … …  
    159206         * Echo the output of get_display.
    160207         *
     208         * @since 2.1.1
     209         *
    161210         * @access public
    162211         * @param int|null $post_id Post ID.
    163212         */
    164213        public static function display( $post_id = null ) {
    165                 echo self::get_display( $post_id ); // phpcs:ignore
     214                echo self::get_display( $post_id ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The view escapes its output as it is built.
    166215        }
    167216
    168217        /**
    169218         * Output the post kind content to the post content.
     219         *
     220         * @since 2.0.2
    170221         *
    171222         * @access public
    … …  
    183234                }
    184235                global $wp_current_filter;
    185                 // Don't allow to be added to the_content more than once (prevent infinite loops)
     236                // Don't allow to be added to the_content more than once (prevent infinite loops).
    186237                $done = false;
    187238                foreach ( $wp_current_filter as $filter ) {
    … …  
    203254         * Output the post kind content to the feed item content.
    204255         *
     256         * @since 3.0.5
     257         *
    205258         * @access public
    206259         *
    … …  
    219272         * Output the post kind content to the content if the `jsonfeed` plugin is active.
    220273         *
     274         * @since 3.0.5
     275         *
    221276         * @access public
    222277         *
    … …  
    228283
    229284                $kind_post = new Kind_Post( $post );
    230                 $kind      = $kind_post->get_kind();
    231                 $type      = Kind_Taxonomy::get_kind_info( $kind, 'property' );
    232                 $cite      = mf2_to_jf2( $kind_post->get_cite() );
     285                $cite      = \ParseThis\mf2_to_jf2( $kind_post->get_cite() );
    233286
    234287                if ( is_string( $cite ) ) {
    … …  
    248301         * Append the post kind display to excerpts.
    249302         *
     303         * @since 2.3.3
     304         *
    250305         * @access public
    251306         *
    … …  
    270325         * This function will remove the www.prefix if it is part of the URL.
    271326         *
     327         * @since 2.3.7
     328         *
    272329         * @access public
    273330         *
    … …  
    283340         * Converts an array of attributes and output them as a string.
    284341         *
     342         * @since 2.1.1
     343         *
    285344         * @access public
    286345         *
    … …  
    301360        /**
    302361         * Converts a URL into a complete `<a>` link with link text.
     362         *
     363         * @since 2.1.1
    303364         *
    304365         * @access public
    … …  
    316377                                $atr = self::get_attributes( $atr );
    317378                }
    318                 $return = '<a ' . $atr . ' href="' . $url . '">' . $name . '</a>';
     379                $return = '<a ' . $atr . ' href="' . esc_url( $url ) . '">' . esc_html( $name ) . '</a>';
    319380                return $return;
    320381        }
    … …  
    323384         * Create formatted HTML output for a field.
    324385         *
    325          * @access public
    326          *
    327          * @param string $field Content to put in the markup
     386         * @since 2.2.0
     387         *
     388         * @access public
     389         *
     390         * @param string $field Content to put in the markup.
    328391         * @param string $attr  Attributes to add to the tag markup.
    329392         * @param string $type  HTML tag type to create. Default span.
    … …  
    333396        public static function get_formatted( $field, $attr, $type = 'span' ) {
    334397                if ( ! isset( $field ) ) {
    335                         return $string;
    336                 }
     398                        return '';
     399                }
     400                $type   = tag_escape( $type );
    337401                $string = '<' . $type . $attr . '>' . $field . '</' . $type . '>';
    338402                return $string;
    … …  
    341405        /**
    342406         * Return post kind-wrapped oEmbed content for a provided URL.
     407         *
     408         * @since 2.1.1
    343409         *
    344410         * @access public
    … …  
    363429                        'dailymotion.com',
    364430                        'flickr.com',
    365                         'imgur.com',
     431                        'imgur.com', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- A domain whose links are embedded with oEmbed, not a remote asset.
    366432                        'issuu.com',
    367433                        'kickstarter.com',
    … …  
    385451                        'youtube.com',
    386452                );
     453                /**
     454                 * Filters the domains whose cited URLs are embedded with oEmbed.
     455                 *
     456                 * @since 3.3.6
     457                 *
     458                 * @param string[] $approvelist Domain names.
     459                 */
    387460                $approvelist = apply_filters( 'post_kind_embed_approvelist', $approvelist );
    388461                if ( ! in_array( $host, $approvelist, true ) ) {
    389462                        return '';
    390463                }
    391                 if ( isset( $GLOBALS['wp_embed'] ) ) {
    392                         $embed = $GLOBALS['wp_embed']->autoembed( $url );
    393                 }
     464                if ( ! isset( $GLOBALS['wp_embed'] ) ) {
     465                        return '';
     466                }
     467                $embed = $GLOBALS['wp_embed']->autoembed( $url );
    394468                if ( 0 === strcmp( $embed, $url ) ) {
    395469                        $embed = '';
    396470                } else {
    397                         $embed = sprintf( '<div class="kind-embed">%1$s<a class="u-url" href="%2$s"></a></div>', $embed, $url );
     471                        // The embed is the oEmbed markup WordPress generated.
     472                        $embed = sprintf( '<div class="kind-embed">%1$s<a class="u-url" href="%2$s"></a></div>', $embed, esc_url( $url ) );
    398473                }
    399474                        return $embed;
    … …  
    402477        /**
    403478         * Returns an array of domains with the post type terminologies
     479         *
     480         * @since 2.1.1
    404481         *
    405482         * @access public
    … …  
    423500                $domain  = self::extract_domain_name( $url );
    424501                if ( array_key_exists( $domain, $strings ) ) {
     502                        /**
     503                         * Filters the phrase for what a known site's URL points to, such as 'a video' for YouTube.
     504                         *
     505                         * It runs only for the sites listed above; other URLs get "a post".
     506                         *
     507                         * @since 2.0.2
     508                         *
     509                         * @param string $string Translated phrase.
     510                         */
    425511                        return apply_filters( 'kind_post_type_string', $strings[ $domain ] );
    426512                } else {
    … …  
    431517        /**
    432518         * Retrieve/Generate the h-card.
     519         *
     520         * @since 2.1.1
    433521         *
    434522         * @param mixed $author The author to generate Accepts an array or optionally other info.
    … …  
    451539
    452540                /**
    453                  * Filter for alternate retrieval types
    454                  *
    455                  * This could be using WordPress's gravatar system, retrieval by pure URL, etc.
    456                  *
    457                  * @param string|boolean $author Defaults to false, but may return string.
    458                  * @param mixed  $author Data on the author, type optional. Defaults to array.
    459                  * @param array  $args        Arguments passed to get_hcard.
     541                 * Filters the author data before it is turned into an h-card.
     542                 *
     543                 * Use it to look up author details another way, for example from a URL
     544                 * or Gravatar.
     545                 *
     546                 * @since 2.3.0
     547                 *
     548                 * @param mixed $author Author data, usually an array with name, url and photo.
     549                 * @param array $args   Arguments passed to Kind_View::get_hcard().
    460550                 */
    461551                $author = apply_filters( 'get_hcard_data', $author, $args );
    … …  
    472562
    473563                /**
    474                  * Filter for alternate presentation
    475                  *
    476                  * @param string|boolean $card Defaults to false, but may return string.
    477                  * @param mixed  $author Data on the author, type optional. Defaults to array.
    478                  * @param array  $args        Arguments passed to get_hcard.
     564                 * Filters the h-card markup for an author.
     565                 *
     566                 * Return non-empty markup to replace the plugin's own h-card. The markup is
     567                 * output as-is, so it must be escaped.
     568                 *
     569                 * @since 2.3.0
     570                 *
     571                 * @param string $card   H-card markup. Default empty, which uses the plugin's markup.
     572                 * @param array  $author Author data, with name, url and photo.
     573                 * @param array  $args   Arguments passed to Kind_View::get_hcard().
    479574                 */
    480575                $card = apply_filters( 'get_hcard', '', $author, $args );
    … …  
    482577                        return $card;
    483578                }
    484                 // Temporarily drop multi-data on display
    485                 if ( ! empty( $author['url'] ) && is_array( $author['url'] ) ) {
    486                         $author['url'] = $author['url'][0];
    487                 }
    488 
    489                 if ( is_array( $author['name'] ) ) {
    490                                 $author['name'] = $author['name'][0];
    491                 }
    492 
    493                 if ( empty( $author['name'] ) && ! empty( $author['url'] ) ) {
     579                // One card is one author: use the first of any list. Kind_Post::normalize_authors() splits lists into cards.
     580                foreach ( array( 'name', 'url', 'photo' ) as $key ) {
     581                        $value          = isset( $author[ $key ] ) ? $author[ $key ] : '';
     582                        $author[ $key ] = is_array( $value ) ? (string) reset( $value ) : (string) $value;
     583                }
     584
     585                // Escape everything here: author data often comes from other sites.
     586                $author['url']   = esc_url( $author['url'] );
     587                $author['photo'] = esc_url( $author['photo'] );
     588                if ( '' === trim( wp_strip_all_tags( $author['name'] ) ) && ! empty( $author['url'] ) ) {
    494589                        $author['name'] = __( 'an author', 'indieweb-post-kinds' );
    495590                }
     591                $author['name']  = esc_html( wp_strip_all_tags( $author['name'] ) );
     592                $args['width']   = absint( $args['width'] );
     593                $args['height']  = absint( $args['height'] );
    496594
    497595                // If no filter generated the card, generate the card.
    … …  
    502600                                }
    503601                                if ( empty( $author['url'] ) ) {
    504                                         return sprintf( '<img src="%1s" class="h-card u-photo p-author" alt="%2s" width=%3s height=%4s />', $author['photo'], $author['name'], $args['width'], $args['height'] );
     602                                        return sprintf( '<img src="%1$s" class="h-card u-photo p-author" alt="%2$s" width="%3$d" height="%4$d" />', $author['photo'], $author['name'], $args['width'], $args['height'] );
    505603                                } else {
    506                                         return sprintf( '<a class="h-card p-author" href="%1s"><img class="u-photo" src="%2s" alt="%3s" width=%4s height=%5s /></a>', $author['url'], $author['photo'], $author['name'], $args['width'], $args['height'] );
     604                                        return sprintf( '<a class="h-card p-author" href="%1$s"><img class="u-photo" src="%2$s" alt="%3$s" width="%4$d" height="%5$d" /></a>', $author['url'], $author['photo'], $author['name'], $args['width'], $args['height'] );
    507605                                }
    508606                                break;
    509607                        case 'name':
    510                                 return sprintf( '<span class="h-card p-author">%1s</span>', $author['name'] );
     608                                return sprintf( '<span class="h-card p-author">%1$s</span>', $author['name'] );
    511609                        case 'both':
    512610                                if ( ! empty( $author['photo'] ) ) {
    513611                                        if ( empty( $author['url'] ) ) {
    514                                                 return sprintf( '<span class="h-card p-author"><img src="%1s" class="u-photo" alt="%2s" width=%3s height=%4s />%5s</span>', $author['photo'], $author['name'], $args['width'], $args['height'], $author['name'] );
     612                                                return sprintf( '<span class="h-card p-author"><img src="%1$s" class="u-photo" alt="%2$s" width="%3$d" height="%4$d" />%5$s</span>', $author['photo'], $author['name'], $args['width'], $args['height'], $author['name'] );
    515613                                        } else {
    516                                                 return sprintf( '<a href="%1s" class="h-card p-author"><img class="u-photo" src="%2s" alt="%3s" width=%4s height=%5s />%6s</a>', $author['url'], $author['photo'], $author['name'], $args['width'], $args['height'], $author['name'] );
     614                                                return sprintf( '<a href="%1$s" class="h-card p-author"><img class="u-photo" src="%2$s" alt="%3$s" width="%4$d" height="%5$d" />%6$s</a>', $author['url'], $author['photo'], $author['name'], $args['width'], $args['height'], $author['name'] );
    517615                                        }
     616                                } elseif ( ! empty( $author['url'] ) ) {
     617                                        return sprintf( '<a href="%1$s" class="h-card p-author">%2$s</a>', $author['url'], $author['name'] );
    518618                                } else {
    519                                         return sprintf( '<span class="h-card p-author">%1s</span>', $author['name'] );
     619                                        return sprintf( '<span class="h-card p-author">%1$s</span>', $author['name'] );
    520620                                }
    521621                                break;
    … …  
    529629         * Retrieve a title for a given citation.
    530630         *
     631         * @since 2.3.0
     632         *
    531633         * @access public
    532634         *
    … …  
    538640                        return false;
    539641                }
    540                 if ( empty( $cite['url'] ) ) {
    541                         if ( ! isset( $cite['name'] ) ) {
     642                // FIXME: Temporary Fix for array functionality.
     643                $url  = isset( $cite['url'] ) ? $cite['url'] : '';
     644                $url  = esc_url( is_array( $url ) ? (string) reset( $url ) : (string) $url );
     645                $name = isset( $cite['name'] ) ? $cite['name'] : null;
     646                $name = is_array( $name ) ? (string) reset( $name ) : $name;
     647                if ( empty( $url ) ) {
     648                        if ( null === $name ) {
    542649                                return '';
    543650                        }
    544                         return sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
    545                 }
    546                 // FIXME: Temporary Fix for array functionality
    547                 if ( is_array( $cite['url'] ) ) {
    548                         $cite['url'] = $cite['url'][0];
    549                 }
    550                 if ( ! array_key_exists( 'name', $cite ) ) {
    551                         // $cite['name'] = self::get_post_type_string( $cite['url'] );
    552                         $cite['name'] = $cite['url'];
    553                 }
    554                 return sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $cite['url'], $cite['name'] );
     651                        return sprintf( '<span class="p-name">%1$s</span>', esc_html( wp_strip_all_tags( (string) $name ) ) );
     652                }
     653                if ( null === $name || '' === $name ) {
     654                        $name = $url;
     655                }
     656                return sprintf( '<a href="%1$s" class="p-name u-url">%2$s</a>', $url, esc_html( wp_strip_all_tags( (string) $name ) ) );
    555657        }
    556658
    557659        /**
    558660         * Retrieve site name for given citation.
     661         *
     662         * @since 2.3.0
    559663         *
    560664         * @access public
    … …  
    570674                        return false;
    571675                }
    572                 return sprintf( '<span class="p-publication">%1s</span>', $cite['publication'] );
    573         }
    574 
    575         /**
    576          * Returns a requested Rating item.
    577          *
    578          * The returned value will be a printf-ready translated string.
    579          *
    580          * @access
    581          *
    582          * @param int $rating Rating Value.
    583          * @return mixed|string
     676                $publication = is_array( $cite['publication'] ) ? (string) reset( $cite['publication'] ) : (string) $cite['publication'];
     677                return sprintf( '<span class="p-publication">%1$s</span>', esc_html( wp_strip_all_tags( $publication ) ) );
     678        }
     679
     680        /**
     681         * Returns the citation's summary as escaped plain text.
     682         *
     683         * Summaries are plain text: tags are stripped, and the text is marked up as
     684         * p-summary. Line breaks are kept.
     685         *
     686         * @since 4.0.0
     687         *
     688         * @access public
     689         *
     690         * @param array $cite Normalized citation (see Kind_Post::normalize_cite()).
     691         * @return string The summary markup, or an empty string if there is none.
     692         */
     693        public static function get_summary( $cite ) {
     694                if ( ! is_array( $cite ) || empty( $cite['summary'] ) ) {
     695                        return '';
     696                }
     697                $summary = $cite['summary'];
     698                if ( is_array( $summary ) ) {
     699                        foreach ( array( 'value', 'text', 'html' ) as $key ) {
     700                                if ( isset( $summary[ $key ] ) ) {
     701                                        $summary = $summary[ $key ];
     702                                        break;
     703                                }
     704                        }
     705                        $summary = is_array( $summary ) ? (string) reset( $summary ) : $summary;
     706                }
     707                $summary = trim( wp_strip_all_tags( (string) $summary ) );
     708                if ( '' === $summary ) {
     709                        return '';
     710                }
     711                return sprintf( '<blockquote class="p-summary">%1$s</blockquote>', nl2br( esc_html( $summary ) ) );
     712        }
     713
     714        /**
     715         * Returns a rating as a row of stars.
     716         *
     717         * @since 3.7.0
     718         *
     719         * @access public
     720         *
     721         * @param int|string $rating Rating value. Values above 10 show 10 stars.
     722         * @return string Plain text: one star per point, or an empty string.
    584723         */
    585724        public static function rating_text( $rating ) {
    586                 if ( ! $rating || 0 === $rating ) {
    587                         return '';
    588                 }
    589 
    590                 $ret = '';
    591                 for ( $i = $rating; $i > 0; $i-- ) {
    592                         $ret .= '⭐';
    593                 }
    594 
    595                 return $ret;
    596         }
    597 
    598         /**
    599          * Returns a requested RSVP option item.
    600          *
    601          * The returned value will be a printf-ready translated string.
    602          *
    603          * @access
    604          *
    605          * @param string $type RSVP type to return.
    606          * @return mixed|string
    607          */
    608         public static function rsvp_text( $type ) {
    609                 if ( ! $type ) {
    610                         return '';
    611                 }
     725                $rating = is_numeric( $rating ) ? (int) $rating : 0;
     726                if ( $rating <= 0 ) {
     727                        return '';
     728                }
     729                return str_repeat( '⭐', min( $rating, 10 ) );
     730        }
     731
     732        /**
     733         * Returns the text for an RSVP.
     734         *
     735         * Called with only a type, it returns a printf-ready string whose
     736         * %1$s and %2$s are the event URL and name, as before 4.0.0; the caller
     737         * must escape both. With a URL or name, it returns the escaped sentence
     738         * with the event linked.
     739         *
     740         * @since 2.6.1
     741         *
     742         * @access public
     743         *
     744         * @param string      $type RSVP value: yes, no, maybe, interested or remote.
     745         * @param string|null $url  Optional. Event URL.
     746         * @param string|null $name Optional. Event name. Defaults to the URL.
     747         * @return string The RSVP text, or an empty string for an unknown type.
     748         */
     749        public static function rsvp_text( $type, $url = null, $name = null ) {
    612750                $rsvp = array(
    613                         /* translators: URL for link to event and name of event */
    614                         'yes'        => __( 'Attending <a href="%1$s" class="u-in-reply-to">%2$s</a>', 'indieweb-post-kinds' ),
    615                         /* translators: URL for link to event and name of event */
    616                         'maybe'      => __( 'Might be attending <a href="%1$s" class="u-in-reply-to">%2$s</a>', 'indieweb-post-kinds' ),
    617                         /* translators: URL for link to event and name of event */
    618                         'no'         => __( 'Unable to Attend <a href="%1$s" class="u-in-reply-to">%2$s</a>', 'indieweb-post-kinds' ),
    619                         /* translators: URL for link to event and name of event */
    620                         'interested' => __( 'Interested in Attending <a href="%1$s" class=u-in-reply-to">%2$s</a>', 'indieweb-post-kinds' ),
    621                         /* translators: URL for link to event and name of event */
    622                         'remote'     => __( 'Attending <a href="%1$s" class="u-in-reply-to">%2$s</a> remotely', 'indieweb-post-kinds' ),
     751                        /* translators: %s: Link to the event */
     752                        'yes'        => __( 'Attending %s', 'indieweb-post-kinds' ),
     753                        /* translators: %s: Link to the event */
     754                        'maybe'      => __( 'Might be attending %s', 'indieweb-post-kinds' ),
     755                        /* translators: %s: Link to the event */
     756                        'no'         => __( 'Unable to Attend %s', 'indieweb-post-kinds' ),
     757                        /* translators: %s: Link to the event */
     758                        'interested' => __( 'Interested in Attending %s', 'indieweb-post-kinds' ),
     759                        /* translators: %s: Link to the event */
     760                        'remote'     => __( 'Attending %s remotely', 'indieweb-post-kinds' ),
    623761                );
    624                 return $rsvp[ $type ];
     762                if ( ! is_string( $type ) || ! array_key_exists( $type, $rsvp ) ) {
     763                        return '';
     764                }
     765                if ( null === $url && null === $name ) {
     766                        return sprintf( esc_html( $rsvp[ $type ] ), '<a href="%1$s" class="u-in-reply-to">%2$s</a>' );
     767                }
     768                $url  = esc_url( (string) $url );
     769                $name = ( null === $name || '' === $name ) ? $url : $name;
     770                $name = esc_html( wp_strip_all_tags( (string) $name ) );
     771                if ( '' === $url ) {
     772                        $link = sprintf( '<span class="p-in-reply-to">%1$s</span>', $name );
     773                } else {
     774                        $link = sprintf( '<a href="%1$s" class="u-in-reply-to">%2$s</a>', $url, $name );
     775                }
     776                return sprintf( esc_html( $rsvp[ $type ] ), $link );
    625777        }
    626778
    627779        /**
    628780         * Returns a requested read status option item.
     781         *
     782         * @since 3.0.7
    629783         *
    630784         * @access public
    … …  
    642796                        'finished' => __( 'Finished Reading: ', 'indieweb-post-kinds' ),
    643797                );
    644                 return $read[ $type ];
     798                return ( is_string( $type ) && isset( $read[ $type ] ) ) ? $read[ $type ] : '';
    645799        }
    646800
    … …  
    648802         * Return a string for a requested duration.
    649803         *
    650          * @access public
    651          *
    652          * @param string|Dateinterval $interval Duration to display.
    653          * @return string
    654          * @throws Exception
     804         * @since 2.6.1
     805         * @since 4.0.0 Returns escaped markup.
     806         *
     807         * @access public
     808         *
     809         * @param string|DateInterval $interval Duration to display.
     810         * @return string The duration as an escaped dt-duration time element, or an empty string if it is not a valid duration.
    655811         */
    656812        public static function display_duration( $interval ) {
    657                 if ( ! $interval instanceof DateInterval ) {
    658                         $interval = new DateInterval( $interval );
    659                 }
     813                $interval = kind_safe_interval( $interval );
    660814                if ( ! $interval ) {
    661815                        return '';
    … …  
    695849                }
    696850
    697                 return sprintf( '<time class="dt-duration" datetime="%1$s">%2$s</time>', date_interval_to_iso8601( $interval ), implode( ' ', $duration ) );
     851                return sprintf( '<time class="dt-duration" datetime="%1$s">%2$s</time>', esc_attr( date_interval_to_iso8601( $interval ) ), esc_html( implode( ' ', $duration ) ) );
    698852        }
    699853}  // End Class
  • indieweb-post-kinds/trunk/includes/class-mf2-post.php

    r2973430 r3738687  
    11<?php
     2/**
     3 * MF2_Post class.
     4 *
     5 * @package Post_Kinds
     6 * @since   3.1.0
     7 */
     8
     9defined( 'ABSPATH' ) || exit;
     10
    211/**
    312 * MF2 Post Class
    413 *
    5  * @package Post Kinds
    6  * Assists in retrieving/saving microformats 2 properties from a post
     14 * Assists in retrieving/saving microformats 2 properties from a post.
     15 *
     16 * @since 3.1.0
     17 * @deprecated 4.0.0 Use Kind_Post. MF2_Post will be removed in a future major version.
    718 */
    819class MF2_Post implements ArrayAccess {
     20        /**
     21         * Post ID.
     22         *
     23         * @since 3.1.0
     24         * @var int
     25         */
    926        public $uid;
     27
     28        /**
     29         * ID of the post author.
     30         *
     31         * @since 3.1.0
     32         * @var int|string
     33         */
    1034        public $post_author;
     35
     36        /**
     37         * Post type.
     38         *
     39         * @since 3.3.0
     40         * @var string
     41         */
    1142        public $post_type;
     43
     44        /**
     45         * Author, as an h-card.
     46         *
     47         * @since 3.1.0
     48         * @var array|string|false
     49         */
    1250        public $author;
     51
     52        /**
     53         * Publication the post appeared in.
     54         *
     55         * @since 3.1.0
     56         * @var string
     57         */
    1358        public $publication;
     59
     60        /**
     61         * Published date, in DATE_W3C format.
     62         *
     63         * @since 3.1.0
     64         * @var string
     65         */
    1466        public $published;
     67
     68        /**
     69         * Updated date, in DATE_W3C format.
     70         *
     71         * @since 3.1.0
     72         * @var string
     73         */
    1574        public $updated;
     75
     76        /**
     77         * Content, with 'html' and 'value' versions.
     78         *
     79         * @since 3.1.0
     80         * @var array
     81         */
    1682        public $content;
     83
     84        /**
     85         * Summary (excerpt).
     86         *
     87         * @since 3.1.0
     88         * @var string
     89         */
    1790        public $summary;
     91
     92        /**
     93         * ID of the parent post.
     94         *
     95         * @since 3.1.0
     96         * @var int
     97         */
    1898        public $post_parent;
     99
     100        /**
     101         * Post kind slug, or media type for an attachment.
     102         *
     103         * @since 3.1.0
     104         * @var string|false|null
     105         */
    19106        public $kind;
     107
     108        /**
     109         * Permalink, or file URL for an attachment.
     110         *
     111         * @since 3.1.0
     112         * @var string|false
     113         */
    20114        public $url;
     115
     116        /**
     117         * Post title.
     118         *
     119         * @since 3.1.0
     120         * @var string
     121         */
    21122        public $name;
     123
     124        /**
     125         * Category and tag names.
     126         *
     127         * @since 3.1.0
     128         * @var string[]
     129         */
    22130        public $category = array();
     131
     132        /**
     133         * URL of the featured image.
     134         *
     135         * @since 3.1.0
     136         * @var string
     137         */
    23138        public $featured;
     139
     140        /**
     141         * Properties stored in mf2_ prefixed post meta, without the prefix.
     142         *
     143         * @since 3.1.0
     144         * @var array
     145         */
    24146        private $mf2 = array();
    25147
     148        /**
     149         * Reads a post's properties.
     150         *
     151         * @since 3.1.0
     152         * @deprecated 4.0.0 Use Kind_Post.
     153         *
     154         * @param int|WP_Post|string $post Post ID, post object, or the URL of a post or attachment.
     155         */
    26156        public function __construct( $post ) {
     157                _deprecated_function( __METHOD__, '4.0.0', 'Kind_Post' );
    27158                if ( is_numeric( $post ) ) {
    28159                        $this->uid = (int) $post;
    … …  
    75206        }
    76207
     208        /**
     209         * Returns the published date.
     210         *
     211         * For an attachment, this is the date stored in post meta.
     212         *
     213         * @since 3.3.0
     214         *
     215         * @return string|false The date, or false if there is none.
     216         */
    77217        public function get_published() {
    78218                if ( 'attachment' === $this->post_type ) {
    79                         return $this->meta['published'] ?? '';
     219                        return $this->get_single( $this->mf2['published'] ?? '' );
    80220                }
    81221                return get_the_date( DATE_W3C, $this->uid );
    82222        }
    83223
     224        /**
     225         * Returns the updated date.
     226         *
     227         * For an attachment, this is the date stored in post meta.
     228         *
     229         * @since 3.3.0
     230         *
     231         * @return string|false The date, or false if there is none.
     232         */
    84233        public function get_updated() {
    85234                if ( 'attachment' === $this->post_type ) {
    86                         return $this->meta['updated'] ?? '';
     235                        return $this->get_single( $this->mf2['updated'] ?? '' );
    87236                }
    88237                return get_the_modified_date( DATE_W3C, $this->uid );
    89238        }
    90239
     240        /**
     241         * Returns the publication the post appeared in.
     242         *
     243         * For posts other than attachments, this is the site title.
     244         *
     245         * @since 3.3.0
     246         *
     247         * @return string The publication, or an empty string if an attachment has none.
     248         */
    91249        public function get_publication() {
    92250                if ( 'attachment' !== $this->post_type ) {
    93251                        return get_bloginfo( 'title' );
    94252                }
    95                 return $this->meta['publication'] ?? '';
    96         }
    97 
     253                return $this->get_single( $this->mf2['publication'] ?? '' );
     254        }
     255
     256        /**
     257         * Whether a property exists, for ArrayAccess.
     258         *
     259         * @since 3.2.0
     260         *
     261         * @param string $offset Property name.
     262         * @return bool
     263         */
     264        #[\ReturnTypeWillChange]
    98265        public function offsetExists( $offset ) {
    99266                $vars = get_object_vars( $this );
    … …  
    104271        }
    105272
     273        /**
     274         * Returns a property, for ArrayAccess.
     275         *
     276         * @since 3.2.0
     277         *
     278         * @param string $offset Property name.
     279         * @return mixed The value, or null if it does not exist.
     280         */
     281        #[\ReturnTypeWillChange]
    106282        public function offsetGet( $offset ) {
    107283                $vars = get_object_vars( $this );
    … …  
    115291        }
    116292
     293        /**
     294         * Sets a property, for ArrayAccess.
     295         *
     296         * @since 3.2.0
     297         *
     298         * @param string $offset Property name.
     299         * @param mixed  $value  The value.
     300         */
     301        #[\ReturnTypeWillChange]
    117302        public function offsetSet( $offset, $value ) {
    118303                $this->set( $offset, $value );
    119304        }
    120305
     306        /**
     307         * Deletes a property stored in post meta, for ArrayAccess.
     308         *
     309         * @since 3.2.0
     310         *
     311         * @param string $offset Property name.
     312         */
     313        #[\ReturnTypeWillChange]
    121314        public function offsetUnset( $offset ) {
    122315                $this->delete( $offset );
    123316        }
    124317
     318        /**
     319         * Returns the names of a post's categories and tags.
     320         *
     321         * 'Uncategorized' is left out.
     322         *
     323         * @since 3.2.0
     324         *
     325         * @param int $post_id Post ID.
     326         * @return string[] Category and tag names.
     327         */
    125328        public function get_categories( $post_id ) {
    126329                $category = array();
    127                 // Get a list of categories and extract their names
     330                // Get a list of categories and extract their names.
    128331                $post_categories = get_the_terms( $post_id, 'category' );
    129332                if ( ! empty( $post_categories ) && ! is_wp_error( $post_categories ) ) {
    … …  
    131334                }
    132335
    133                 // Get a list of tags and extract their names
     336                // Get a list of tags and extract their names.
    134337                $post_tags = get_the_terms( $post_id, 'post_tag' );
    135338                if ( ! empty( $post_tags ) && ! is_wp_error( $post_tags ) ) {
    … …  
    142345        }
    143346
     347        /**
     348         * Returns the post kind, or the media type of an attachment.
     349         *
     350         * @since 3.1.0
     351         *
     352         * @return string|false|null The kind slug, or null for an attachment that is not image, video or audio.
     353         */
    144354        private function get_post_kind() {
    145355                if ( is_attachment( $this->uid ) ) {
    … …  
    166376        }
    167377
     378        /**
     379         * Returns the post object.
     380         *
     381         * @since 3.1.0
     382         *
     383         * @return WP_Post|null The post, or null if it does not exist.
     384         */
    168385        public function get_post() {
    169386                return get_post( $this->uid );
    … …  
    172389        /**
    173390         * Is prefix in string.
     391         *
     392         * @since 3.1.0
    174393         *
    175394         * @param  string $source The source string.
    … …  
    183402        /**
    184403         * Returns True if Array is Multidimensional.
     404         *
     405         * @since 3.1.0
    185406         *
    186407         * @param array $arr array.
    … …  
    196417        }
    197418
     419        /**
     420         * Sanitizes HTML with the allowed tags from the plugin settings.
     421         *
     422         * @since 3.1.0
     423         *
     424         * @param mixed $value The value. Anything but a string is returned unchanged.
     425         * @return mixed The sanitized value.
     426         */
    198427        public static function sanitize_content( $value ) {
    199428                if ( ! is_string( $value ) ) {
    … …  
    207436        }
    208437
     438        /**
     439         * Sanitizes a URL or text, recursively.
     440         *
     441         * @since 3.1.0
     442         *
     443         * @param string|array $value The value.
     444         * @return string|array The sanitized value.
     445         */
    209446        public function sanitize_text( $value ) {
    210447                if ( is_array( $value ) ) {
    … …  
    222459         * Retrieve author
    223460         *
     461         * @since 3.1.0
     462         *
    224463         * @return boolean|array The result or false if does not exist.
    225464         */
    226465        public function get_author() {
    227466                if ( ! $this->post_author ) {
    228                         return $this->meta['author'] ?? false;
    229                 }
    230                 // Attachments may have been uploaded by a user but may have metadata for original author
     467                        return $this->get_single( $this->mf2['author'] ?? false );
     468                }
     469                // Attachments may have been uploaded by a user but may have metadata for original author.
    231470                if ( 'attachment' === $this->post_type ) {
    232                         return $this->meta['author'] ?? '';
     471                        return $this->get_single( $this->mf2['author'] ?? '' );
    233472                }
    234473                return array(
    … …  
    243482
    244483        /**
    245          * Sets an array with only the mf2 prefixed meta.
    246          *
     484         * Returns the properties stored in mf2_ prefixed post meta, without the prefix.
     485         *
     486         * @since 3.1.0
    247487         */
    248488        private function get_mf2meta() {
    … …  
    251491                        return array();
    252492                }
    253                 if ( isset( $meta['response'] ) ) {
    254                         $response = maybe_unserialize( $meta['response'] );
    255                         // Retrieve from the old response array and store in new location.
    256                         if ( ! empty( $response ) ) {
     493                // Very old versions stored the citation in the response meta. Read it as
     494                // mf2_cite when there is no newer citation; the upgrade routine moves it.
     495                if ( isset( $meta['response'] ) && ! isset( $meta['mf2_cite'] ) ) {
     496                        $response = maybe_unserialize( $meta['response'][0] );
     497                        if ( is_array( $response ) ) {
    257498                                $new = array();
    258                                 // Convert to new format and update.
    259499                                if ( ! empty( $response['title'] ) ) {
    260500                                        $new['name'] = $response['title'];
    … …  
    270510                                }
    271511                                if ( ! empty( $response['author'] ) ) {
    272                                         $new['card']         = array();
    273                                         $new['card']['name'] = $response['author'];
     512                                        $new['card'] = array( 'name' => $response['author'] );
    274513                                        if ( ! empty( $response['icon'] ) ) {
    275514                                                $new['card']['photo'] = $response['icon'];
    276515                                        }
    277516                                }
    278                                 $new         = array_unique( $new );
    279                                 $new['card'] = array_unique( $new['card'] );
    280                                 if ( isset( $new ) ) {
    281                                         update_post_meta( $this->uid, 'mf2_cite', $new );
    282                                         delete_post_meta( $this->uid, 'response' );
    283                                         $meta['cite'] = $new;
     517                                if ( $new ) {
     518                                        $meta['mf2_cite'] = array( $new );
    284519                                }
    285520                        }
    … …  
    291526                                unset( $meta[ $key ] );
    292527                                $key = str_replace( 'mf2_', '', $key );
    293                                 // Do not save microput prefixed instructions
     528                                // Do not save microput prefixed instructions.
    294529                                if ( self::str_prefix( $key, 'mp-' ) ) {
    295530                                        continue;
    … …  
    312547         * Map Properties Based on Post Type
    313548         *
     549         * @since 3.3.0
    314550         */
    315551        private function get_post_type_properties() {
    … …  
    325561         * Retrieve value
    326562         *
    327          * @param  string $key The key to retrieve.
    328          * @param  boolean $single Whether to return a a single value or array if there is only one value.
     563         * @since 3.1.0
     564         *
     565         * @param string|null $key    Optional. The key to retrieve. Default null, for every property.
     566         * @param bool        $single Optional. Whether to unwrap a single value from its array. Default true.
    329567         * @return boolean|string|array The result or false if does not exist.
    330568         */
    … …  
    374612        }
    375613
     614        /**
     615         * Whether the post has a property.
     616         *
     617         * @since 3.1.0
     618         *
     619         * @param string $key Property name.
     620         * @return bool
     621         */
    376622        public function has_key( $key ) {
    377623                $keys = array_merge( get_object_vars( $this ), $this->mf2 );
    … …  
    379625        }
    380626
     627        /**
     628         * Unwraps single-item arrays, recursively.
     629         *
     630         * @since 3.1.0
     631         *
     632         * @param mixed $value   The value to unwrap.
     633         * @param bool  $discard Optional. Whether to keep only the first item of a list
     634         *                       with more than one item. Default false.
     635         * @return mixed The value, with single-item arrays replaced by their item.
     636         */
    381637        private function single_array( $value, $discard = false ) {
    382638                if ( ! is_array( $value ) ) {
    … …  
    395651        }
    396652
     653        /**
     654         * Stores a property, or several.
     655         *
     656         * @since 3.1.0
     657         *
     658         * @param string|array $key   Property name, an array of property => value pairs, or an mf2 item.
     659         * @param mixed        $value Optional. The value. Default null.
     660         * @return mixed The result of storing the value, or false or null if nothing was stored.
     661         */
    397662        public function set( $key, $value = null ) {
    398663                if ( ! $key ) {
    … …  
    542807        }
    543808
     809        /**
     810         * Deletes a property stored in post meta.
     811         *
     812         * @since 3.1.0
     813         *
     814         * @param string $key Property name.
     815         * @return bool True on success, false on failure.
     816         */
    544817        public function delete( $key ) {
    545818                return delete_post_meta( $this->uid, 'mf2_' . $key );
    546819        }
    547820
     821        /**
     822         * Converts mf2 to jf2.
     823         *
     824         * @since 3.1.0
     825         *
     826         * @param array $cite The mf2 item.
     827         * @return array The jf2 item.
     828         */
    548829        public function mf2_to_jf2( $cite ) {
    549830                return mf2_to_jf2( $cite );
    550831        }
    551832
     833        /**
     834         * Returns the first item of an array.
     835         *
     836         * @since 3.1.0
     837         *
     838         * @param mixed $value The value.
     839         * @return mixed The first item, or the value if it is not an array.
     840         */
    552841        public function get_single( $value ) {
    553842                if ( is_array( $value ) ) {
    … …  
    557846        }
    558847
     848        /**
     849         * Converts jf2 to mf2.
     850         *
     851         * @since 3.1.0
     852         *
     853         * @param array  $item The jf2 item.
     854         * @param string $type Optional. jf2 type to use if the item has none. Default 'cite'.
     855         * @return array The mf2 item.
     856         */
    559857        public function jf2_to_mf2( $item, $type = 'cite' ) {
    560858                if ( is_array( $item ) && isset( $item['type'] ) && ! isset( $item['properties'] ) ) {
    … …  
    565863        }
    566864
    567         // Retrieve the right property to use for the link preview based on the kind.
    568         // It will return an array of properties or false if it cannot find what it needs.
    569         // Also will update old posts with new settings
     865        /**
     866         * Returns a property for the link preview, as jf2.
     867         *
     868         * Updates a property stored in the format used before 2.7.0.
     869         *
     870         * @since 3.1.0
     871         *
     872         * @param string $property Property name.
     873         * @return array|false The property, or false if there is none.
     874         */
    570875        public function fetch( $property ) {
    571876
    572                 // If the property is not set then exit
     877                // If the property is not set then exit.
    573878                if ( ! $property || ! $this->has_key( $property ) ) {
    574879                        return false;
    … …  
    578883                        $return = array_shift( $return );
    579884                }
    580                 // If it is in fact a string it is the pre 2.7.0 format and should be updated
     885                // If it is in fact a string it is the pre 2.7.0 format and should be updated.
    581886                if ( is_string( $return ) ) {
    582887                        if ( $this->has_key( 'cite' ) ) {
    … …  
    596901        }
    597902
     903        /**
     904         * Returns the IDs of media attached to a post.
     905         *
     906         * @since 3.1.0
     907         *
     908         * @param string      $type MIME type, such as 'image'.
     909         * @param int|WP_Post $post Post ID or post object.
     910         * @return int[] Attachment IDs.
     911         */
    598912        public function get_attached_media( $type, $post ) {
    599913                $posts = get_attached_media( $type, $post );
    … …  
    601915        }
    602916
     917        /**
     918         * Returns the post's audio.
     919         *
     920         * @since 3.1.0
     921         *
     922         * @return int[]|false Attachment IDs, or false if there are none.
     923         */
    603924        public function get_audios() {
    604925                // Check if the post itself if an audio attachment.
    … …  
    615936        }
    616937
     938        /**
     939         * Returns the post's videos.
     940         *
     941         * @since 3.1.0
     942         *
     943         * @return int[]|false Attachment IDs, or false if there are none.
     944         */
    617945        public function get_videos() {
    618946                // Check if the post itself if an audio attachment.
    … …  
    629957        }
    630958
     959        /**
     960         * Returns the post's images.
     961         *
     962         * @since 3.1.0
     963         *
     964         * @param bool $content_allow Optional. Whether to return images in the content. Default false.
     965         * @return int[]|false Attachment IDs, or false if there are none.
     966         */
    631967        public function get_images( $content_allow = false ) {
    632968                // Check if the post itself is an image attachment.
    … …  
    636972                $post_content = $this->content['html'] ?? '';
    637973                if ( $post_content ) {
     974                        // Recorded when the post is saved; see Kind_Media_Metadata::save_post().
    638975                        $att_ids = get_post_meta( $this->uid, '_content_img_ids', true );
    639                         if ( false === $att_ids ) {
    640                                 $att_ids = Kind_Media_Metadata::get_img_from_content( $post->post_content );
    641                                 update_post_meta( $this->uid, '_content_img_ids', $att_ids );
    642                         }
    643976                        if ( $att_ids ) {
    644977                                return $content_allow ? $att_ids : array();
    … …  
    648981                        }
    649982                }
    650                 // If there is a featured image return only that. Otherwise return all images
     983                // If there is a featured image return only that. Otherwise return all images.
    651984                $featured = get_post_thumbnail_id( $this->uid );
    652985                if ( $featured ) {
    … …  
    655988                $att_ids = $this->get_attached_media( 'image', $this->uid );
    656989                $photos  = $this->get( 'photo', false );
    657                 if ( is_array( $photos ) ) {
    658                         if ( ! wp_is_numeric_array( $photos ) ) {
    659                                 $photos = array( $photos );
    660                         }
    661 
    662                         $photos = $this->sideload_images( $photos );
    663                         $this->set( 'photo', $photos );
     990                if ( is_array( $photos ) && ! wp_is_numeric_array( $photos ) ) {
     991                        $photos = array( $photos );
    664992                }
    665993                $att_ids = array_merge( $att_ids, $this->get_attachments_from_urls( $photos ) );
    … …  
    670998        }
    671999
    672         private function media_sideload_image( $url, $post_id, $description = null ) {
    673                 // To prevent sideloading the same image multiple times check for the original URL which will now be stored
    674                 $ids = get_posts(
    675                         array(
    676                                 'post_type'        => 'attachment',
    677                                 'suppress_filters' => false,
    678                                 'nopaging'         => true,
    679                                 'meta_key'         => 'mf2_url',
    680                                 'meta_value'       => $url,
    681                                 'fields'           => 'ids',
    682                         )
    683                 );
    684                 if ( ! empty( $ids ) ) {
    685                         return $ids[0];
    686                 }
    687                 $id = media_sideload_image( $url, $post_id, $description, 'id' );
    688                 if ( $id ) {
    689                         update_post_meta( $id, 'mf2_url', $url );
    690                 }
    691                 return $id;
    692         }
    693 
    694 
    695         private function sideload_images( $photos ) {
    696                 require_once ABSPATH . 'wp-admin/includes/media.php';
    697                 require_once ABSPATH . 'wp-admin/includes/file.php';
    698                 require_once ABSPATH . 'wp-admin/includes/image.php';
    699                 foreach ( $photos as $key => $value ) {
    700                         if ( is_string( $value ) ) {
    701                                 if ( ! wp_http_validate_url( $value ) ) {
    702                                         continue;
    703                                 } elseif ( ! attachment_url_to_postid( $value ) ) {
    704                                         $id = self::media_sideload_image( $value, $this->uid );
    705                                         if ( $id ) {
    706                                                 $photos[ $key ] = wp_get_attachment_url( $id );
    707                                         }
    708                                 }
    709                         }
    710                         // Attempt to normalize old data
    711                         if ( is_array( $value ) ) {
    712                                 $value = mf2_to_jf2( $value );
    713                                 $id    = attachment_url_to_postid( $value['url'] );
    714                                 if ( ! $id ) {
    715                                         $id = self::media_sideload_image( $value['url'], $this->uid );
    716                                         if ( $id ) {
    717                                                 $value['url'] = wp_get_attachment_url( $id );
    718                                         }
    719                                 }
    720                                 $args = array(
    721                                         'ID'           => $id,
    722                                         'post_title'   => $value['name'] ?? '',
    723                                         'post_excerpt' => $value['summary'] ?? '',
    724                                 );
    725                                 $args = array_filter( $args );
    726                                 wp_update_post( $args );
    727                                 unset( $value['name'] );
    728                                 unset( $value['summary'] );
    729                                 foreach ( $value as $k => $v ) {
    730                                         update_post_meta( $id, 'mf2_' . $k, $v );
    731                                 }
    732                                 $photos[ $key ] = $value['url'];
    733                         }
    734                 }
    735                 return $photos;
    736         }
    737 
     1000        /**
     1001         * Looks up the attachment IDs for media URLs.
     1002         *
     1003         * @since 3.1.0
     1004         *
     1005         * @param string|array $urls A URL, or a list of URLs or arrays with a 'url' key.
     1006         * @return int[] Attachment IDs found.
     1007         */
    7381008        public function get_attachments_from_urls( $urls ) {
    7391009                if ( is_string( $urls ) ) {
  • indieweb-post-kinds/trunk/includes/class-post-kind.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Post Kind Class
     3 * Post_Kind class.
    44 *
    5  * @package Post Kind
    6  * Used to define a Post Kind object
     5 * @package Post_Kinds
     6 * @since   3.1.0
    77 */
    88
    9 final class Post_Kind {
    10         public $id; // Term ID
    11         public $slug; // Kind Slug
    12         public $name; // Name of Kind - Plural
    13         public $singular_name; // Name of Kind - Singular
    14         public $verb; // The string for the verb or action (liked this)
    15         public $format; // Post Format that Maps to This
    16         public $icon; // Icon
     9defined( 'ABSPATH' ) || exit;
     10
     11/**
     12 * Defines a single post kind, as registered with register_post_kind().
     13 *
     14 * @since 3.1.0
     15 */
     16final class Post_Kind implements JsonSerializable {
     17        /**
     18         * Term ID.
     19         *
     20         * @since 3.1.0
     21         * @var int
     22         */
     23        public $id;
     24
     25        /**
     26         * Kind slug.
     27         *
     28         * @since 3.1.0
     29         * @var string
     30         */
     31        public $slug;
     32
     33        /**
     34         * General name for the kind, plural.
     35         *
     36         * @since 3.1.0
     37         * @var string
     38         */
     39        public $name;
     40
     41        /**
     42         * Name for one instance of the kind.
     43         *
     44         * @since 3.1.0
     45         * @var string
     46         */
     47        public $singular_name;
     48
     49        /**
     50         * The verb or action, such as 'Liked'.
     51         *
     52         * @since 3.1.0
     53         * @var string
     54         */
     55        public $verb;
     56
     57        /**
     58         * Post format that maps to this kind.
     59         *
     60         * @since 3.1.0
     61         * @var string
     62         */
     63        public $format;
     64
     65        /**
     66         * Icon name.
     67         *
     68         * @since 3.1.0
     69         * @var string
     70         */
     71        public $icon;
     72
     73        /**
     74         * Description of the kind.
     75         *
     76         * @since 3.1.0
     77         * @var string
     78         */
    1779        public $description;
     80
     81        /**
     82         * Link to more information about the kind.
     83         *
     84         * @since 3.1.0
     85         * @var string
     86         */
    1887        public $description_url;
    19         public $title; // Should this Kind Have an Explicit Title
    20         public $show; // Show in Settings
    21         public $property; // Primary Property
    22         public $properties; // Array of Properties
    23         public $shortlink; // Shortlink Coding per http://tantek.pbworks.com/w/page/21743973/Whistle#design
    24 
     88
     89        /**
     90         * Whether the kind should have an explicit title.
     91         *
     92         * @since 3.1.0
     93         * @var bool
     94         */
     95        public $title;
     96
     97        /**
     98         * Whether to show the kind in Settings.
     99         *
     100         * @since 3.1.0
     101         * @var bool
     102         */
     103        public $show;
     104
     105        /**
     106         * Primary microformats 2 property, such as 'like-of'.
     107         *
     108         * @since 3.1.0
     109         * @var string
     110         */
     111        public $property;
     112
     113        /**
     114         * The fields for the kind. See Kind_Fields.
     115         *
     116         * @since 3.1.0
     117         * @var array
     118         */
     119        public $properties;
     120
     121        /**
     122         * Shortlink code, per http://tantek.pbworks.com/w/page/21743973/Whistle#design.
     123         *
     124         * @since 3.4.0
     125         * @var string
     126         */
     127        public $shortlink;
     128
     129        /**
     130         * Arguments given at registration that are not declared properties, such as
     131         * those a custom kind adds. Kept here rather than as dynamic properties,
     132         * which are deprecated in PHP 8.2, and still readable as $kind->name.
     133         *
     134         * @since 4.0.0
     135         * @var array
     136         */
     137        private $extra = array();
     138
     139        /**
     140         * Defines a kind.
     141         *
     142         * @since 3.1.0
     143         *
     144         * @param string $slug Kind slug.
     145         * @param array  $args Optional. Kind arguments. See Kind_Taxonomy::register_post_kind().
     146         */
    25147        public function __construct( $slug, $args = array() ) {
    26148                $this->slug = $slug;
    … …  
    28150        }
    29151
     152        /**
     153         * Sets the kind's properties from its arguments, filling in defaults.
     154         *
     155         * Arguments that are not declared properties are kept as extra arguments.
     156         *
     157         * @since 3.1.0
     158         *
     159         * @param array $args Kind arguments. See Kind_Taxonomy::register_post_kind().
     160         */
    30161        public function set_props( $args ) {
    31162                $defaults = array(
    … …  
    35166                        'format'        => 'standard',
    36167                        'icon'          => $this->slug,
    37                         'show'          => 'false',
     168                        'show'          => false,
    38169                        'property'      => '',
    39170                );
    40171                $args     = wp_parse_args( $args, $defaults );
    41172                foreach ( $args as $property_name => $property_value ) {
    42                         $this->$property_name = $property_value;
     173                        if ( 'extra' !== $property_name && property_exists( $this, $property_name ) ) {
     174                                $this->$property_name = $property_value;
     175                        } else {
     176                                $this->extra[ $property_name ] = $property_value;
     177                        }
    43178                }
    44179        }
     180
     181        /**
     182         * Reads an extra argument given at registration.
     183         *
     184         * @since 4.0.0
     185         *
     186         * @param string $name Argument name.
     187         * @return mixed The value, or null if it was not given.
     188         */
     189        public function __get( $name ) {
     190                return isset( $this->extra[ $name ] ) ? $this->extra[ $name ] : null;
     191        }
     192
     193        /**
     194         * Whether an extra argument was given at registration.
     195         *
     196         * @since 4.0.0
     197         *
     198         * @param string $name Argument name.
     199         * @return bool
     200         */
     201        public function __isset( $name ) {
     202                return isset( $this->extra[ $name ] );
     203        }
     204
     205        /**
     206         * Sets an extra argument, without creating a dynamic property.
     207         *
     208         * @since 4.0.0
     209         *
     210         * @param string $name  Argument name.
     211         * @param mixed  $value Value.
     212         */
     213        public function __set( $name, $value ) {
     214                $this->extra[ $name ] = $value;
     215        }
     216
     217        /**
     218         * Whether the kind has a declared property or an extra argument of this name.
     219         *
     220         * @since 4.0.0
     221         *
     222         * @param string $name Property or argument name.
     223         * @return bool
     224         */
     225        public function has( $name ) {
     226                return ( 'extra' !== $name && property_exists( $this, $name ) ) || array_key_exists( $name, $this->extra );
     227        }
     228
     229        /**
     230         * Returns the kind's declared properties and extra arguments, for JSON.
     231         *
     232         * @since 4.0.0
     233         *
     234         * @return array
     235         */
     236        #[\ReturnTypeWillChange]
     237        public function jsonSerialize() {
     238                $vars = get_object_vars( $this );
     239                unset( $vars['extra'] );
     240                return array_merge( $this->extra, $vars );
     241        }
    45242}
  • indieweb-post-kinds/trunk/includes/compat.php

    r2973430 r3738687  
    11<?php
    2 
    3 // get_feed_build_date introduced in WordPress 5.2
    4 if ( ! function_exists( 'get_feed_build_date' ) ) {
    5 
    6         function get_feed_build_date( $format ) {
    7                 global $wp_query;
    8 
    9                 if ( empty( $wp_query ) || ! $wp_query->have_posts() ) {
    10                         // Fallback to last time any post was modified or published.
    11                         return get_lastpostmodified( 'GMT' );
    12                 }
    13 
    14                         // Extract the post modified times from the posts.
    15                         $modified_times = wp_list_pluck( $wp_query->posts, 'post_modified_gmt' );
    16 
    17                         // If this is a comment feed, check those objects too.
    18                 if ( $wp_query->is_comment_feed() && $wp_query->comment_count ) {
    19                         // Extract the comment modified times from the comments.
    20                         $comment_times = wp_list_pluck( $wp_query->comments, 'comment_date_gmt' );
    21 
    22                                 // Add the comment times to the post times for comparison.
    23                                 $modified_times = array_merge( $modified_times, $comment_times );
    24                 }
    25 
    26                         // Determine the maximum modified time.
    27                         $max_modified_time = max(
    28                                 array_map(
    29                                         function ( $time ) use ( $format ) {
    30                                                                                 return mysql2date( $format, $time, false );
    31                                         },
    32                                         $modified_times
    33                                 )
    34                         );
    35 
    36                         /**
    37                          * Filters the date the last post or comment in the query was modified.
    38                          *
    39                          * @since 5.2.0
    40                          *
    41                          * @param string $max_modified_time Date the last post or comment was modified in the query.
    42                          * @param string $format            The date format requested in get_feed_build_date.
    43                          */
    44                         return apply_filters( 'get_feed_build_date', $max_modified_time, $format );
     2/**
     3 * Polyfills for functions added to WordPress or PHP after the plugin's minimums
     4 * (WordPress 6.2, PHP 7.4).
     5 *
     6 * Each polyfill is a copy of the core function, guarded by function_exists(),
     7 * and notes the version that introduced it so it can be removed once the
     8 * minimum catches up. ClassicPress may already provide some of them.
     9 *
     10 * @package Post_Kinds
     11 */
     12
     13defined( 'ABSPATH' ) || exit;
     14
     15// wp_get_admin_notice() was introduced in WordPress 6.4.
     16if ( ! function_exists( 'wp_get_admin_notice' ) ) {
     17        /**
     18         * Creates and returns the markup for an admin notice.
     19         *
     20         * @since 6.4.0
     21         *
     22         * @param string $message The message.
     23         * @param array  $args {
     24         *     Optional. An array of arguments for the admin notice. Default empty array.
     25         *
     26         *     @type string   $type               Optional. The type of admin notice.
     27         *                                        For example, 'error', 'success', 'warning', 'info'.
     28         *                                        Default empty string.
     29         *     @type bool     $dismissible        Optional. Whether the admin notice is dismissible. Default false.
     30         *     @type string   $id                 Optional. The value of the admin notice's ID attribute. Default empty string.
     31         *     @type string[] $additional_classes Optional. A string array of class names. Default empty array.
     32         *     @type string[] $attributes         Optional. Additional attributes for the notice div. Default empty array.
     33         *     @type bool     $paragraph_wrap     Optional. Whether to wrap the message in paragraph tags. Default true.
     34         * }
     35         * @return string The markup for an admin notice.
     36         */
     37        function wp_get_admin_notice( $message, $args = array() ) {
     38                $defaults = array(
     39                        'type'               => '',
     40                        'dismissible'        => false,
     41                        'id'                 => '',
     42                        'additional_classes' => array(),
     43                        'attributes'         => array(),
     44                        'paragraph_wrap'     => true,
     45                );
     46
     47                $args = wp_parse_args( $args, $defaults );
     48
     49                /**
     50                 * Filters the arguments for an admin notice.
     51                 *
     52                 * @since 6.4.0
     53                 *
     54                 * @param array  $args    The arguments for the admin notice.
     55                 * @param string $message The message for the admin notice.
     56                 */
     57                $args       = apply_filters( 'wp_admin_notice_args', $args, $message );
     58                $id         = '';
     59                $classes    = 'notice';
     60                $attributes = '';
     61
     62                if ( is_string( $args['id'] ) ) {
     63                        $trimmed_id = trim( $args['id'] );
     64
     65                        if ( '' !== $trimmed_id ) {
     66                                $id = 'id="' . $trimmed_id . '" ';
     67                        }
     68                }
     69
     70                if ( is_string( $args['type'] ) ) {
     71                        $type = trim( $args['type'] );
     72
     73                        if ( str_contains( $type, ' ' ) ) {
     74                                _doing_it_wrong(
     75                                        __FUNCTION__,
     76                                        sprintf(
     77                                                /* translators: %s: The "type" key. */
     78                                                esc_html__( 'The %s key must be a string without spaces.', 'default' ),
     79                                                '<code>type</code>'
     80                                        ),
     81                                        '6.4.0'
     82                                );
     83                        }
     84
     85                        if ( '' !== $type ) {
     86                                $classes .= ' notice-' . $type;
     87                        }
     88                }
     89
     90                if ( true === $args['dismissible'] ) {
     91                        $classes .= ' is-dismissible';
     92                }
     93
     94                if ( is_array( $args['additional_classes'] ) && ! empty( $args['additional_classes'] ) ) {
     95                        $classes .= ' ' . implode( ' ', $args['additional_classes'] );
     96                }
     97
     98                if ( is_array( $args['attributes'] ) && ! empty( $args['attributes'] ) ) {
     99                        $attributes = '';
     100                        foreach ( $args['attributes'] as $attr => $val ) {
     101                                if ( is_bool( $val ) ) {
     102                                        $attributes .= $val ? ' ' . $attr : '';
     103                                } elseif ( is_int( $attr ) ) {
     104                                        $attributes .= ' ' . esc_attr( trim( $val ) );
     105                                } elseif ( $val ) {
     106                                        $attributes .= ' ' . $attr . '="' . esc_attr( trim( $val ) ) . '"';
     107                                }
     108                        }
     109                }
     110
     111                if ( false !== $args['paragraph_wrap'] ) {
     112                        $message = "<p>$message</p>";
     113                }
     114
     115                $markup = sprintf( '<div %1$sclass="%2$s"%3$s>%4$s</div>', $id, $classes, $attributes, $message );
     116
     117                /**
     118                 * Filters the markup for an admin notice.
     119                 *
     120                 * @since 6.4.0
     121                 *
     122                 * @param string $markup  The HTML markup for the admin notice.
     123                 * @param string $message The message for the admin notice.
     124                 * @param array  $args    The arguments for the admin notice.
     125                 */
     126                return apply_filters( 'wp_admin_notice_markup', $markup, $message, $args );
    45127        }
    46128}
    47129
    48 // Backcompat for wp_filter_content_tags introduced in 5.5.
    49 if ( ! function_exists( 'wp_filter_content_tags' ) ) {
    50         function wp_filter_content_tags( $content ) {
    51                 return wp_make_content_images_responsive( $content ); // phpcs:ignore
     130// wp_admin_notice() was introduced in WordPress 6.4.
     131if ( ! function_exists( 'wp_admin_notice' ) ) {
     132        /**
     133         * Outputs an admin notice.
     134         *
     135         * @since 6.4.0
     136         *
     137         * @param string $message The message to output.
     138         * @param array  $args {
     139         *     Optional. An array of arguments for the admin notice. Default empty array.
     140         *
     141         *     @type string   $type               Optional. The type of admin notice.
     142         *                                        For example, 'error', 'success', 'warning', 'info'.
     143         *                                        Default empty string.
     144         *     @type bool     $dismissible        Optional. Whether the admin notice is dismissible. Default false.
     145         *     @type string   $id                 Optional. The value of the admin notice's ID attribute. Default empty string.
     146         *     @type string[] $additional_classes Optional. A string array of class names. Default empty array.
     147         *     @type string[] $attributes         Optional. Additional attributes for the notice div. Default empty array.
     148         *     @type bool     $paragraph_wrap     Optional. Whether to wrap the message in paragraph tags. Default true.
     149         * }
     150         */
     151        function wp_admin_notice( $message, $args = array() ) {
     152                /**
     153                 * Fires before an admin notice is output.
     154                 *
     155                 * @since 6.4.0
     156                 *
     157                 * @param string $message The message for the admin notice.
     158                 * @param array  $args    The arguments for the admin notice.
     159                 */
     160                do_action( 'wp_admin_notice', $message, $args );
     161
     162                echo wp_kses_post( wp_get_admin_notice( $message, $args ) );
    52163        }
    53164}
     165
     166// wp_trigger_error() was introduced in WordPress 6.4 (ClassicPress 2.2.0).
     167if ( ! function_exists( 'wp_trigger_error' ) ) {
     168        /**
     169         * Generates a user-level error/warning/notice/deprecation message.
     170         *
     171         * Generates the message when `WP_DEBUG` is true.
     172         *
     173         * @since 6.4.0
     174         *
     175         * @param string $function_name The function that triggered the error.
     176         * @param string $message       The message explaining the error.
     177         *                              The message can contain allowed HTML 'a' (with href), 'code',
     178         *                              'br', 'em', and 'strong' tags and http or https protocols.
     179         *                              If it contains other HTML tags or protocols, the message should be escaped
     180         *                              before passing to this function to avoid being stripped {@see wp_kses()}.
     181         * @param int    $error_level   Optional. The designated error type for this error.
     182         *                              Only works with E_USER family of constants. Default E_USER_NOTICE.
     183         *
     184         * @throws WP_Exception If the error level is E_USER_ERROR. Exception where WP_Exception does not exist.
     185         */
     186        function wp_trigger_error( $function_name, $message, $error_level = E_USER_NOTICE ) {
     187
     188                // Bail out if WP_DEBUG is not turned on.
     189                if ( ! WP_DEBUG ) {
     190                        return;
     191                }
     192
     193                /**
     194                 * Fires when the given function triggers a user-level error/warning/notice/deprecation message.
     195                 *
     196                 * Can be used for debug backtracking.
     197                 *
     198                 * @since 6.4.0
     199                 *
     200                 * @param string $function_name The function that was called.
     201                 * @param string $message       A message explaining what has been done incorrectly.
     202                 * @param int    $error_level   The designated error type for this error.
     203                 */
     204                do_action( 'wp_trigger_error_run', $function_name, $message, $error_level );
     205
     206                if ( ! empty( $function_name ) ) {
     207                        $message = sprintf( '%s(): %s', $function_name, $message );
     208                }
     209
     210                $message = wp_kses(
     211                        $message,
     212                        array(
     213                                'a'      => array( 'href' => true ),
     214                                'br'     => array(),
     215                                'code'   => array(),
     216                                'em'     => array(),
     217                                'strong' => array(),
     218                        ),
     219                        array( 'http', 'https' )
     220                );
     221
     222                if ( E_USER_ERROR === $error_level ) {
     223                        // WP_Exception was also introduced in WordPress 6.4.
     224                        $exception = class_exists( 'WP_Exception' ) ? 'WP_Exception' : 'Exception';
     225                        throw new $exception( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
     226                }
     227
     228                trigger_error( $message, $error_level ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error, WordPress.Security.EscapeOutput.OutputNotEscaped
     229        }
     230}
  • indieweb-post-kinds/trunk/includes/kind-functions.php

    r3014138 r3738687  
    11<?php
    22/**
    3  * Kind Functions
     3 * Global functions for registering, reading and displaying kinds.
    44 *
    5  * Global Scoped Functions for Handling Kinds.
    6  *
    7  * @package Post Kinds
     5 * @package Post_Kinds
     6 * @since   2.0.2
    87 */
    98
    10 /**
    11  *
    12  */
    13 function register_post_kind( $slug, $args ) {
    14         Kind_Taxonomy::register_post_kind( $slug, $args );
    15 }
    16 
    17 function set_post_kind_visibility( $slug, $show = true ) {
    18         Kind_Taxonomy::set_post_kind_visibility( $slug, $show );
    19 }
    20 
    21 /**
    22  * Retrieves an array of post kind slugs.
    23  *
    24  * @return array The array of post kind slugs.
    25  */
    26 function get_post_kind_slugs() {
    27         return Kind_Taxonomy::get_post_kind_slugs();
    28 }
    29 
    30 /**
    31  * Returns a pretty, translated version of a post kind slug
    32  *
    33  * @param string $slug A post format slug.
    34  * @return string The translated post format name.
    35  */
    36 function get_post_kind_string( $slug ) {
    37         return Kind_Taxonomy::get_post_kind_string( $slug );
    38 }
    39 
    40 /**
    41  * Returns a link to a post kind index.
    42  *
    43  * @param string $kind The post kind slug.
    44  * @return string The post kind term link.
    45  */
    46 function get_post_kind_link( $kind ) {
    47         return Kind_Taxonomy::get_post_kind_link( $kind );
    48 }
    49 
    50 /**
    51  * Returns the post kind slug for the current post.
    52  *
    53  * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
    54  * @return string The post kind slug.
    55  */
    56 function get_post_kind_slug( $post = null ) {
    57         return Kind_Taxonomy::get_post_kind_slug( $post );
    58 }
    59 
    60 /**
    61  * Returns the post kind name for the current post.
    62  *
    63  * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
    64  * @return string The post kind name.
    65  */
    66 function get_post_kind( $post = null ) {
    67         return Kind_Taxonomy::get_post_kind( $post );
    68 }
    69 
    70 /**
    71  * Check if a post has any of the given kinds, or any kind.
    72  *
    73  * @uses has_term()
    74  *
    75  * @param string|array $kinds Optional. The kind to check.
    76  * @param object|int   $post Optional. The post to check. If not supplied, defaults to the current post if used in the loop.
    77  * @return bool True if the post has any of the given kinds (or any kind, if no kind specified), false otherwise.
    78  */
    79 function has_post_kind( $kinds = array(), $post = null ) {
    80         return Kind_Taxonomy::has_post_kind( $kinds, $post );
    81 }
    82 
    83 /**
    84  * Assign a kind to a post
    85  *
    86  * @param int|object $post The post for which to assign a kind.
    87  * @param string     $kind A kind to assign. Using an empty string or array will default to note.
    88  * @return mixed WP_Error on error. Array of affected term IDs on success.
    89  */
    90 function set_post_kind( $post, $kind ) {
    91         return Kind_Taxonomy::set_post_kind( $post, $kind );
    92 }
    93 
    94 /**
    95  * Return the Displayed Response for a Specific Kind
    96  *
    97  * @param $slug
    98  * @param $name
    99  * @param $args
    100  * @return string
    101  */
    102 
    103 function get_kind_view_part( $slug, $name = null, $args = null ) {
    104         Kind_View::get_view_part( $slug, $name, $args );
    105 }
    106 
    107 function kind_display( $post_id = null ) {
    108                 echo Kind_View::get_display( $post_id ); // phpcs:ignore
    109 }
    110 
    111 function kind_flatten_array( $a ) {
    112         if ( ! is_array( $a ) ) {
     9defined( 'ABSPATH' ) || exit;
     10
     11if ( ! function_exists( 'register_post_kind' ) ) {
     12        /**
     13         * Registers a post kind.
     14         *
     15         * @since 3.1.0
     16         *
     17         * @param string $slug Post kind slug.
     18         * @param array  $args Post kind arguments. See Kind_Taxonomy::register_post_kind().
     19         */
     20        function register_post_kind( $slug, $args ) {
     21                Kind_Taxonomy::register_post_kind( $slug, $args );
     22        }
     23}
     24
     25if ( ! function_exists( 'set_post_kind_visibility' ) ) {
     26        /**
     27         * Shows or hides a post kind in Settings.
     28         *
     29         * @since 3.1.0
     30         *
     31         * @param string $slug Post kind slug.
     32         * @param bool   $show Optional. Whether to show the kind. Default true.
     33         */
     34        function set_post_kind_visibility( $slug, $show = true ) {
     35                Kind_Taxonomy::set_post_kind_visibility( $slug, $show );
     36        }
     37}
     38
     39if ( ! function_exists( 'get_post_kind_slugs' ) ) {
     40        /**
     41         * Retrieves an array of post kind slugs.
     42         *
     43         * @since 0.30
     44         *
     45         * @return array The array of post kind slugs.
     46         */
     47        function get_post_kind_slugs() {
     48                return Kind_Taxonomy::get_kind_list();
     49        }
     50}
     51
     52if ( ! function_exists( 'get_post_kind_string' ) ) {
     53        /**
     54         * Returns a pretty, translated version of a post kind slug.
     55         *
     56         * @since 0.30
     57         *
     58         * @param string $slug A post kind slug.
     59         * @return string The translated post kind name, or an empty string if the kind is not registered.
     60         */
     61        function get_post_kind_string( $slug ) {
     62                return Kind_Taxonomy::get_post_kind_string( $slug );
     63        }
     64}
     65
     66if ( ! function_exists( 'get_post_kind_link' ) ) {
     67        /**
     68         * Returns a link to a post kind index.
     69         *
     70         * @since 0.30
     71         *
     72         * @param string $kind The post kind slug.
     73         * @return string|WP_Error|false The post kind term link, a WP_Error from get_term_link(),
     74         *                               or false if the kind has no term.
     75         */
     76        function get_post_kind_link( $kind ) {
     77                return Kind_Taxonomy::get_post_kind_link( $kind );
     78        }
     79}
     80
     81if ( ! function_exists( 'get_post_kind_slug' ) ) {
     82        /**
     83         * Returns the post kind slug for the current post.
     84         *
     85         * @since 0.30
     86         *
     87         * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
     88         * @return string|false The post kind slug, or false if the post has no kind.
     89         */
     90        function get_post_kind_slug( $post = null ) {
     91                return Kind_Taxonomy::get_post_kind_slug( $post );
     92        }
     93}
     94
     95if ( ! function_exists( 'get_post_kind' ) ) {
     96        /**
     97         * Returns the post kind name for the current post.
     98         *
     99         * @since 0.30
     100         *
     101         * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
     102         * @return string|false The translated post kind name, or false if the post has no kind.
     103         */
     104        function get_post_kind( $post = null ) {
     105                return Kind_Taxonomy::get_post_kind( $post );
     106        }
     107}
     108
     109if ( ! function_exists( 'has_post_kind' ) ) {
     110        /**
     111         * Check if a post has any of the given kinds, or any kind.
     112         *
     113         * @since 0.30
     114         *
     115         * @uses has_term()
     116         *
     117         * @param string|array $kinds Optional. The kind to check.
     118         * @param WP_Post|int  $post  Optional. The post to check. If not supplied, defaults to the current post if used in the loop.
     119         * @return bool True if the post has any of the given kinds (or any kind, if no kind specified), false otherwise.
     120         */
     121        function has_post_kind( $kinds = array(), $post = null ) {
     122                return Kind_Taxonomy::has_post_kind( $kinds, $post );
     123        }
     124}
     125
     126if ( ! function_exists( 'set_post_kind' ) ) {
     127        /**
     128         * Assign a kind to a post.
     129         *
     130         * @since 0.30
     131         *
     132         * @param int|WP_Post $post The post for which to assign a kind.
     133         * @param string      $kind A registered kind's slug.
     134         * @return array|false|WP_Error Array of affected term IDs on success. WP_Error if the post
     135         *                              or the kind is not valid, or false on failure.
     136         */
     137        function set_post_kind( $post, $kind ) {
     138                return Kind_Taxonomy::set_post_kind( $post, $kind );
     139        }
     140}
     141
     142if ( ! function_exists( 'get_kind_view_part' ) ) {
     143        /**
     144         * Returns the rendered view for a kind.
     145         *
     146         * @since 2.5.0
     147         *
     148         * @param string     $slug View slug, such as 'kind'.
     149         * @param string     $name Optional. Kind slug, such as 'like'. Default null.
     150         * @param array|null $args Optional. Arguments for the view. See Kind_View::get_view_part(). Default null.
     151         * @return string The view's markup.
     152         */
     153        function get_kind_view_part( $slug, $name = null, $args = null ) {
     154                return Kind_View::get_view_part( $slug, $name, $args );
     155        }
     156}
     157
     158if ( ! function_exists( 'kind_display' ) ) {
     159        /**
     160         * Outputs the kind markup for a post.
     161         *
     162         * For themes that place the markup themselves instead of adding it to the content.
     163         *
     164         * @since 2.6.1
     165         *
     166         * @param int|null $post_id Optional. Post ID. Default the current post.
     167         */
     168        function kind_display( $post_id = null ) {
     169                        echo Kind_View::get_display( $post_id ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The view escapes its output as it is built.
     170        }
     171}
     172
     173if ( ! function_exists( 'kind_flatten_array' ) ) {
     174        /**
     175         * Removes empty values from an array and unwraps single-item lists, recursively.
     176         *
     177         * @since 2.7.5
     178         *
     179         * @param mixed $a The value to flatten.
     180         * @return mixed The flattened value. An empty string if nothing is left.
     181         */
     182        function kind_flatten_array( $a ) {
     183                if ( ! is_array( $a ) ) {
     184                        return $a;
     185                }
     186                if ( wp_is_numeric_array( $a ) ) {
     187                        $a = array_values( array_filter( array_map( 'kind_flatten_array', $a ) ) );
     188                } else {
     189                        $a = array_filter( $a );
     190                }
     191                if ( empty( $a ) ) {
     192                        return '';
     193                }
     194                if ( 1 === count( $a ) && isset( $a[0] ) ) {
     195                        return $a[0];
     196                }
    113197                return $a;
    114198        }
    115         if ( wp_is_numeric_array( $a ) ) {
    116                 $array = array_map( 'kind_flatten_array', $a );
    117         }
    118         $array = array_filter( $a );
    119         if ( 1 === count( $a ) ) {
    120                 return $a[0];
    121         }
    122 }
    123 
    124 // Return any sort of src urls in content
    125 function kind_src_url_in_content( $content ) {
    126         if ( ! $content ) {
     199}
     200
     201if ( ! function_exists( 'kind_src_url_in_content' ) ) {
     202        /**
     203         * Returns every src attribute value in some markup.
     204         *
     205         * @since 3.0.0
     206         *
     207         * @param string $content The markup.
     208         * @return string[]|int The src URLs, or 0 if there are none.
     209         */
     210        function kind_src_url_in_content( $content ) {
     211                if ( ! $content ) {
     212                        return 0;
     213                }
     214                if ( preg_match_all( '@src="([^"]+)"@', $content, $output ) ) {
     215                        return array_pop( $output );
     216                }
    127217                return 0;
    128218        }
    129         if ( preg_match_all( '@src="([^"]+)"@', $content, $output ) ) {
    130                 return array_pop( $output );
    131         }
    132         return 0;
    133 }
    134 
    135 /**
    136  * Get a Marketed Up Link to the Post.
    137  *
    138  * @param WP_Post|null Post to Display.
    139  * @param string|array Classes to add to link
    140  * @param string|array Classes to add to the date
    141  * @return string Marked up link to a post.
    142  **/
    143 
    144 function kind_get_the_link( $post = null, $cls = null, $date_cls = null ) {
    145         $post = get_post( $post );
    146         $kind = get_post_kind_slug( $post );
    147 
    148         if ( is_array( $cls ) ) {
    149                 $cls = implode( ' ', $cls );
    150         }
    151 
    152         if ( is_array( $date_cls ) ) {
    153                 $date_cls = implode( ' ', $date_cls );
    154         }
    155 
    156         $time_string = '<time class="%3$s" datetime="%1$s">%2$s</time>';
    157         $time_string = sprintf(
    158                 $time_string,
    159                 esc_attr( get_the_date( DATE_W3C, $post ) ),
    160                 get_the_date( '', $post ),
    161                 $date_cls
    162         );
    163         return sprintf( '<a class="%4$s" href="%2$s">%1$s</a> - %3$s', kind_get_the_title( $post, $kind ), get_the_permalink( $post ), $time_string, esc_attr( $cls ) );
    164 }
    165 
    166 
    167 /**
    168  * Get a Generated Title for a Post as Most Post Kinds do Not Have an Explicit Title.
    169  *
    170  * @param WP_Post|null Post to Display.
    171  * @param array $args Arguments.
    172  * @return string Marked up link to a post.
    173  **/
    174 function kind_get_the_title( $post = null, $args = array() ) {
    175         $defaults = array(
    176                 'photo_size' => array( 32, 32 ),
    177         );
    178 
    179         $args = wp_parse_args( $args, $defaults );
    180 
    181         $post      = get_post( $post );
    182         $kind      = get_post_kind_slug( $post );
    183         $title     = get_the_title( $post );
    184         $before    = Kind_Taxonomy::get_before_kind( $kind );
    185         $content   = '';
    186         $kind_post = new Kind_Post( $post );
    187 
    188         if ( ! empty( $title ) ) {
    189                 return $title;
    190         }
    191 
    192         if ( in_array( $kind, array( 'audio', 'video', 'photo' ), true ) ) {
    193                 switch ( $kind ) {
    194                         case 'photo':
    195                                 $photos = $kind_post->get_photo();
    196                                 $before = wp_get_attachment_image(
    197                                         $photos[0],
    198                                         $args['photo_size'],
    199                                         false,
    200                                         array(
    201                                                 'class' => 'kind-photo-thumbnail',
    202                                         )
    203                                 );
    204                 }
    205         } elseif ( ! in_array( $kind, array( 'note', 'article' ), true ) ) {
    206                 $cite = $kind_post->get_cite( 'name' );
    207                 if ( false === $cite ) {
    208                         $content = Kind_View::get_post_type_string( $kind_post->get_cite( 'url' ) );
     219}
     220
     221if ( ! function_exists( 'kind_get_the_link' ) ) {
     222        /**
     223         * Returns a marked up link to a post, followed by its date.
     224         *
     225         * @since 3.5.9
     226         *
     227         * @param WP_Post|int|null  $post     Optional. Post to display. Default the current post.
     228         * @param string|array|null $cls      Optional. Classes to add to the link. Default null.
     229         * @param string|array|null $date_cls Optional. Classes to add to the date. Default null.
     230         * @return string Marked up link to a post.
     231         */
     232        function kind_get_the_link( $post = null, $cls = null, $date_cls = null ) {
     233                $post = get_post( $post );
     234                $kind = get_post_kind_slug( $post );
     235
     236                if ( is_array( $cls ) ) {
     237                        $cls = implode( ' ', $cls );
     238                }
     239
     240                if ( is_array( $date_cls ) ) {
     241                        $date_cls = implode( ' ', $date_cls );
     242                }
     243
     244                $time_string = '<time class="%3$s" datetime="%1$s">%2$s</time>';
     245                $time_string = sprintf(
     246                        $time_string,
     247                        esc_attr( get_the_date( DATE_W3C, $post ) ),
     248                        esc_html( get_the_date( '', $post ) ),
     249                        esc_attr( $date_cls )
     250                );
     251                return sprintf( '<a class="%4$s" href="%2$s">%1$s</a> - %3$s', kind_get_the_title( $post, $kind ), esc_url( get_the_permalink( $post ) ), $time_string, esc_attr( $cls ) );
     252        }
     253}
     254
     255if ( ! function_exists( 'kind_get_the_title' ) ) {
     256        /**
     257         * Returns a post's title, generating one if the post has none.
     258         *
     259         * Most post kinds do not have an explicit title.
     260         *
     261         * @since 3.5.9
     262         *
     263         * @param WP_Post|int|null $post Optional. Post to display. Default the current post.
     264         * @param array            $args {
     265         *     Optional. Arguments.
     266         *
     267         *     @type int[]|string $photo_size Image size for a photo post's thumbnail. Default array( 32, 32 ).
     268         * }
     269         * @return string The title, or the kind's icon or name and generated text.
     270         */
     271        function kind_get_the_title( $post = null, $args = array() ) {
     272                $defaults = array(
     273                        'photo_size' => array( 32, 32 ),
     274                );
     275
     276                $args = wp_parse_args( $args, $defaults );
     277
     278                $post      = get_post( $post );
     279                $kind      = get_post_kind_slug( $post );
     280                $title     = get_the_title( $post );
     281                $before    = Kind_Taxonomy::get_before_kind( $kind );
     282                $content   = '';
     283                $kind_post = new Kind_Post( $post );
     284
     285                if ( ! empty( $title ) ) {
     286                        return $title;
     287                }
     288
     289                if ( in_array( $kind, array( 'audio', 'video', 'photo' ), true ) ) {
     290                        switch ( $kind ) {
     291                                case 'photo':
     292                                        $photos = $kind_post->get_photo();
     293                                        $before = wp_get_attachment_image(
     294                                                $photos[0],
     295                                                $args['photo_size'],
     296                                                false,
     297                                                array(
     298                                                        'class' => 'kind-photo-thumbnail',
     299                                                )
     300                                        );
     301                        }
     302                } elseif ( ! in_array( $kind, array( 'note', 'article' ), true ) ) {
     303                        $cite = $kind_post->get_cite( 'name' );
     304                        if ( false === $cite ) {
     305                                $content = Kind_View::get_post_type_string( $kind_post->get_cite( 'url' ) );
     306                        } else {
     307                                $content = $cite;
     308                        }
    209309                } else {
    210                         $content = $cite;
    211                 }
    212         } else {
    213                 $content = $post->post_excerpt;
    214                 // If no excerpt use content
    215                 if ( ! $content ) {
    216                         $content = $post->post_content;
    217                 }
    218                 // If no content use date
    219                 if ( $content ) {
    220                         $content = mb_strimwidth( wp_strip_all_tags( $content ), 0, 40, '...' );
    221                 }
    222         }
    223         if ( is_array( $content ) ) {
    224                 $content = wp_json_encode( $content );
    225         }
    226 
    227         $content = apply_filters( 'kind_get_the_title_content', $content, $post );
    228         $before  = apply_filters( 'kind_get_the_title_before', $before, $post );
    229 
    230         return trim( sprintf( '%1$s %2$s', $before, $content ) );
    231 }
     310                        $content = $post->post_excerpt;
     311                        // If no excerpt use content.
     312                        if ( ! $content ) {
     313                                $content = $post->post_content;
     314                        }
     315                        // If no content use date.
     316                        if ( $content ) {
     317                                $content = mb_strimwidth( wp_strip_all_tags( $content ), 0, 40, '...' );
     318                        }
     319                }
     320                if ( is_array( $content ) ) {
     321                        $content = wp_json_encode( $content );
     322                }
     323                // The content is text (a cited name or an excerpt), often from another site.
     324                $content = esc_html( wp_strip_all_tags( (string) $content ) );
     325
     326                /**
     327                 * Filters the text of a generated post title.
     328                 *
     329                 * The text is a cited name, an excerpt or similar, already escaped. A callback
     330                 * that adds markup must escape what it adds.
     331                 *
     332                 * @since 3.5.9
     333                 *
     334                 * @param string  $content Escaped title text.
     335                 * @param WP_Post $post    The post.
     336                 */
     337                $content = apply_filters( 'kind_get_the_title_content', $content, $post );
     338                /**
     339                 * Filters the markup shown before a generated post title.
     340                 *
     341                 * The markup is output as-is, so it must be escaped.
     342                 *
     343                 * @since 3.5.9
     344                 *
     345                 * @param string  $before The kind's icon or name, from Kind_Taxonomy::get_before_kind().
     346                 * @param WP_Post $post   The post.
     347                 */
     348                $before  = apply_filters( 'kind_get_the_title_before', $before, $post );
     349
     350                return trim( sprintf( '%1$s %2$s', $before, $content ) );
     351        }
     352}
  • indieweb-post-kinds/trunk/includes/register-kinds.php

    r3001401 r3738687  
    11<?php
    2 /* Registers built-in Post Kinds
     2/**
     3 * Registers the built-in kinds.
     4 *
     5 * The arguments each kind takes are documented in
     6 * Kind_Taxonomy::register_post_kind().
     7 *
     8 * @package Post_Kinds
     9 * @since   3.3.4
    310 */
    411
     12defined( 'ABSPATH' ) || exit;
     13
    514register_post_kind(
    615        'article',
    716        array(
    8                 'singular_name'   => __( 'Article', 'indieweb-post-kinds' ), // Name for one instance of the kind
    9                 'name'            => __( 'Articles', 'indieweb-post-kinds' ), // General name for the kind plural
    10                 'verb'            => ' ', // The string for the verb or action (liked this)
    11                 'property'        => '', // microformats 2 property
     17                'singular_name'   => __( 'Article', 'indieweb-post-kinds' ),
     18                'name'            => __( 'Articles', 'indieweb-post-kinds' ),
     19                'verb'            => ' ',
     20                'property'        => '',
    1221                'properties'      => array(),
    13                 'format'          => '', // Post Format that maps to this
     22                'format'          => '',
    1423                'description'     => __( 'traditional long form content: a post with an explicit title and body', 'indieweb-post-kinds' ),
    1524                'description_url' => 'http://indieweb.org/article',
    1625                'shortlink'       => 'b',
    17                 'title'           => true, // Should this kind have an explicit title
    18                 'show'            => true, // Show in Settings
     26                'title'           => true,
     27                'show'            => true,
    1928        )
    2029);
    … …  
    2332        'note',
    2433        array(
    25                 'singular_name'   => __( 'Note', 'indieweb-post-kinds' ), // Name for one instance of the kind
    26                 'name'            => __( 'Notes', 'indieweb-post-kinds' ), // General name for the kind plural
    27                 'verb'            => ' ', // The string for the verb or action (liked this)
    28                 'property'        => '', // microformats 2 property
     34                'singular_name'   => __( 'Note', 'indieweb-post-kinds' ),
     35                'name'            => __( 'Notes', 'indieweb-post-kinds' ),
     36                'verb'            => ' ',
     37                'property'        => '',
    2938                'properties'      => array(),
    30                 'format'          => 'aside', // Post Format that maps to this
     39                'format'          => 'aside',
    3140                'description'     => __( 'short content: a post or status update with just plain content and typically without a title', 'indieweb-post-kinds' ),
    3241                'description_url' => 'http://indieweb.org/note',
    3342                'shortlink'       => 't',
    34                 'title'           => false, // Should this kind have an explicit title
    35                 'show'            => false, // Show in Settings
     43                'title'           => false,
     44                'show'            => false,
    3645        )
    3746);
    … …  
    4049        'reply',
    4150        array(
    42                 'singular_name'   => __( 'Reply', 'indieweb-post-kinds' ), // Name for one instance of the kind
    43                 'name'            => __( 'Replies', 'indieweb-post-kinds' ), // General name for the kind plural
    44                 'verb'            => __( 'Replied to', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    45                 'property'        => 'in-reply-to', // microformats 2 property
     51                'singular_name'   => __( 'Reply', 'indieweb-post-kinds' ),
     52                'name'            => __( 'Replies', 'indieweb-post-kinds' ),
     53                'verb'            => __( 'Replied to', 'indieweb-post-kinds' ),
     54                'property'        => 'in-reply-to',
    4655                'properties'      => array(
    4756                        'in-reply-to' => array(
    … …  
    97106                        ),
    98107                ),
    99                 'format'          => 'link', // Post Format that maps to this
     108                'format'          => 'link',
    100109                'description'     => __( 'a reply to content typically on another site', 'indieweb-post-kinds' ),
    101110                'description_url' => 'http://indieweb.org/reply',
    102111                'shortlink'       => 't',
    103                 'title'           => false, // Should this kind have an explicit title
    104                 'show'            => true, // Show in Settings
     112                'title'           => false,
     113                'show'            => true,
    105114        )
    106115);
    … …  
    109118        'repost',
    110119        array(
    111                 'singular_name'   => __( 'Repost', 'indieweb-post-kinds' ), // Name for one instance of the kind
    112                 'name'            => __( 'Reposts', 'indieweb-post-kinds' ), // General name for the kind plural
    113                 'verb'            => __( 'Reposted', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    114                 'property'        => 'repost-of', // microformats 2 property
     120                'singular_name'   => __( 'Repost', 'indieweb-post-kinds' ),
     121                'name'            => __( 'Reposts', 'indieweb-post-kinds' ),
     122                'verb'            => __( 'Reposted', 'indieweb-post-kinds' ),
     123                'property'        => 'repost-of',
    115124                'properties'      => array(
    116125                        'repost-of' => array(
    … …  
    166175                        ),
    167176                ),
    168                 'format'          => '', // Post Format that maps to this
     177                'format'          => '',
    169178                'description'     => __( 'a complete reposting of content from another site', 'indieweb-post-kinds' ),
    170179                'description_url' => 'http://indieweb.org/repost',
    171180                'shortlink'       => 'b',
    172                 'title'           => true, // Should this kind have an explicit title
    173                 'show'            => true, // Show in Settings
     181                'title'           => true,
     182                'show'            => true,
    174183        )
    175184);
    … …  
    178187        'like',
    179188        array(
    180                 'singular_name'   => __( 'Like', 'indieweb-post-kinds' ), // Name for one instance of the kind
    181                 'name'            => __( 'Likes', 'indieweb-post-kinds' ), // General name for the kind plural
    182                 'verb'            => __( 'Liked', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    183                 'property'        => 'like-of', // microformats 2 property
     189                'singular_name'   => __( 'Like', 'indieweb-post-kinds' ),
     190                'name'            => __( 'Likes', 'indieweb-post-kinds' ),
     191                'verb'            => __( 'Liked', 'indieweb-post-kinds' ),
     192                'property'        => 'like-of',
    184193                'properties'      => array(
    185194                        'like-of' => array(
    … …  
    235244                        ),
    236245                ),
    237                 'format'          => 'link', // Post Format that maps to this
     246                'format'          => 'link',
    238247                'description'     => __( 'a way to pay compliments to the original post/poster of external content', 'indieweb-post-kinds' ),
    239248                'description_url' => 'http://indieweb.org/like',
    240249                'shortlink'       => 'f',
    241                 'title'           => false, // Should this kind have an explicit title
    242                 'show'            => true, // Show in Settings
     250                'title'           => false,
     251                'show'            => true,
    243252        )
    244253);
    … …  
    247256        'favorite',
    248257        array(
    249                 'singular_name'   => __( 'Favorite', 'indieweb-post-kinds' ), // Name for one instance of the kind
    250                 'name'            => __( 'Favorites', 'indieweb-post-kinds' ), // General name for the kind plural
    251                 'verb'            => __( 'Favorited', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    252                 'property'        => 'favorite-of', // microformats 2 property
     258                'singular_name'   => __( 'Favorite', 'indieweb-post-kinds' ),
     259                'name'            => __( 'Favorites', 'indieweb-post-kinds' ),
     260                'verb'            => __( 'Favorited', 'indieweb-post-kinds' ),
     261                'property'        => 'favorite-of',
    253262                'properties'      => array(
    254263                        'favorite-of' => array(
    … …  
    304313                        ),
    305314                ),
    306                 'format'          => 'link', // Post Format that maps to this
     315                'format'          => 'link',
    307316                'description'     => __( 'special to the author', 'indieweb-post-kinds' ),
    308317                'description_url' => 'http://indieweb.org/favorite',
    309318                'shortlink'       => 'f',
    310                 'title'           => false, // Should this kind have an explicit title
    311                 'show'            => true, // Show in Settings
     319                'title'           => false,
     320                'show'            => true,
    312321        )
    313322);
    … …  
    316325        'bookmark',
    317326        array(
    318                 'singular_name'   => __( 'Bookmark', 'indieweb-post-kinds' ), // Name for one instance of the kind
    319                 'name'            => __( 'Bookmarks', 'indieweb-post-kinds' ), // General name for the kind plural
    320                 'verb'            => __( 'Bookmarked', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    321                 'property'        => 'bookmark-of', // microformats 2 property
     327                'singular_name'   => __( 'Bookmark', 'indieweb-post-kinds' ),
     328                'name'            => __( 'Bookmarks', 'indieweb-post-kinds' ),
     329                'verb'            => __( 'Bookmarked', 'indieweb-post-kinds' ),
     330                'property'        => 'bookmark-of',
    322331                'properties'      => array(
    323332                        'bookmark-of' => array(
    … …  
    373382                        ),
    374383                ),
    375                 'format'          => 'link', // Post Format that maps to this
     384                'format'          => 'link',
    376385                'description'     => __( 'storing a link/bookmark for personal use or sharing with others', 'indieweb-post-kinds' ),
    377386                'description_url' => 'http://indieweb.org/bookmark',
    378387                'shortlink'       => 'h',
    379                 'title'           => false, // Should this kind have an explicit title
    380                 'show'            => true, // Show in Settings
     388                'title'           => false,
     389                'show'            => true,
    381390        )
    382391);
    … …  
    385394        'photo',
    386395        array(
    387                 'singular_name'   => __( 'Photo', 'indieweb-post-kinds' ), // Name for one instance of the kind
    388                 'name'            => __( 'Photos', 'indieweb-post-kinds' ), // General name for the kind plural
    389                 'verb'            => ' ', // The string for the verb or action (liked this)
    390                 'property'        => 'photo', // microformats 2 property
    391                 'format'          => 'image', // Post Format that maps to this
     396                'singular_name'   => __( 'Photo', 'indieweb-post-kinds' ),
     397                'name'            => __( 'Photos', 'indieweb-post-kinds' ),
     398                'verb'            => ' ',
     399                'property'        => 'photo',
     400                'format'          => 'image',
    392401                'description'     => __( 'a post with an embedded image/photo as its primary focus', 'indieweb-post-kinds' ),
    393402                'description_url' => 'http://indieweb.org/photo',
    394403                'shortlink'       => 'p',
    395                 'title'           => false, // Should this kind have an explicit title
    396                 'show'            => true, // Show in Settings
     404                'title'           => false,
     405                'show'            => true,
    397406        )
    398407);
    … …  
    401410        'video',
    402411        array(
    403                 'singular_name'   => __( 'Video', 'indieweb-post-kinds' ), // Name for one instance of the kind
    404                 'name'            => __( 'Videos', 'indieweb-post-kinds' ), // General name for the kind plural
    405                 'verb'            => ' ', // The string for the verb or action (liked this)
    406                 'property'        => 'video', // microformats 2 property
    407                 'format'          => 'video', // Post Format that maps to this
     412                'singular_name'   => __( 'Video', 'indieweb-post-kinds' ),
     413                'name'            => __( 'Videos', 'indieweb-post-kinds' ),
     414                'verb'            => ' ',
     415                'property'        => 'video',
     416                'format'          => 'video',
    408417                'description'     => __( 'a post with an embedded video as its primary focus', 'indieweb-post-kinds' ),
    409418                'description_url' => 'http://indieweb.org/video',
    410419                'shortlink'       => 'a',
    411                 'title'           => false, // Should this kind have an explicit title
    412                 'show'            => true, // Show in Settings
     420                'title'           => false,
     421                'show'            => true,
    413422        )
    414423);
    … …  
    417426        'audio',
    418427        array(
    419                 'singular_name'   => __( 'Audio', 'indieweb-post-kinds' ), // Name for one instance of the kind
    420                 'name'            => __( 'Audios', 'indieweb-post-kinds' ), // General name for the kind plural
    421                 'verb'            => ' ', // The string for the verb or action (liked this)
    422                 'property'        => 'audio', // microformats 2 property
    423                 'format'          => 'audio', // Post Format that maps to this
     428                'singular_name'   => __( 'Audio', 'indieweb-post-kinds' ),
     429                'name'            => __( 'Audios', 'indieweb-post-kinds' ),
     430                'verb'            => ' ',
     431                'property'        => 'audio',
     432                'format'          => 'audio',
    424433                'description'     => __( 'a post with an embedded audio file as its primary focus', 'indieweb-post-kinds' ),
    425434                'description_url' => 'http://indieweb.org/audio',
    426435                'shortlink'       => 'a',
    427                 'title'           => false, // Should this kind have an explicit title
    428                 'show'            => true, // Show in Settings
     436                'title'           => false,
     437                'show'            => true,
    429438        )
    430439);
    … …  
    433442        'tag',
    434443        array(
    435                 'singular_name'   => __( 'Tag', 'indieweb-post-kinds' ), // Name for one instance of the kind
    436                 'name'            => __( 'Tags', 'indieweb-post-kinds' ), // General name for the kind plural
    437                 'verb'            => __( 'Tagged', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    438                 'property'        => 'tag-of', // microformats 2 property
    439                 'format'          => 'link', // Post Format that maps to this
     444                'singular_name'   => __( 'Tag', 'indieweb-post-kinds' ),
     445                'name'            => __( 'Tags', 'indieweb-post-kinds' ),
     446                'verb'            => __( 'Tagged', 'indieweb-post-kinds' ),
     447                'property'        => 'tag-of',
     448                'format'          => 'link',
    440449                'description'     => __( 'allows you to tag a post as being of a specific category or tag, or for person tagging', 'indieweb-post-kinds' ),
    441450                'description_url' => 'http://indieweb.org/tag',
    442451                'shortlink'       => 'd',
    443                 'title'           => false, // Should this kind have an explicit title
    444                 'show'            => false, // Show in Settings
     452                'title'           => false,
     453                'show'            => false,
    445454        )
    446455);
    … …  
    449458        'rsvp',
    450459        array(
    451                 'singular_name'   => __( 'RSVP', 'indieweb-post-kinds' ), // Name for one instance of the kind
    452                 'name'            => __( 'RSVPs', 'indieweb-post-kinds' ), // General name for the kind plural
    453                 'verb'            => __( 'RSVPed', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    454                 'property'        => 'in-reply-to', // microformats 2 property
     460                'singular_name'   => __( 'RSVP', 'indieweb-post-kinds' ),
     461                'name'            => __( 'RSVPs', 'indieweb-post-kinds' ),
     462                'verb'            => __( 'RSVPed', 'indieweb-post-kinds' ),
     463                'property'        => 'in-reply-to',
    455464                'properties'      => array(
    456465                        'rsvp'        => array(
    … …  
    509518                        ),
    510519                ),
    511                 'format'          => 'link', // Post Format that maps to this
     520                'format'          => 'link',
    512521                'description'     => __( 'a specific type of reply regarding attendance of an event', 'indieweb-post-kinds' ),
    513522                'description_url' => 'http://indieweb.org/rsvp',
    514523                'shortlink'       => 't',
    515                 'title'           => false, // Should this kind have an explicit title
    516                 'show'            => true, // Show in Settings
     524                'title'           => false,
     525                'show'            => true,
    517526        )
    518527);
    … …  
    521530        'listen',
    522531        array(
    523                 'singular_name'   => __( 'Listen', 'indieweb-post-kinds' ), // Name for one instance of the kind
    524                 'name'            => __( 'Listens', 'indieweb-post-kinds' ), // General name for the kind plural
    525                 'verb'            => __( 'Listened', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    526                 'property'        => 'listen-of', // microformats 2 property
     532                'singular_name'   => __( 'Listen', 'indieweb-post-kinds' ),
     533                'name'            => __( 'Listens', 'indieweb-post-kinds' ),
     534                'verb'            => __( 'Listened', 'indieweb-post-kinds' ),
     535                'property'        => 'listen-of',
    527536                'properties'      => array(
    528537                        'listen-of' => array(
    … …  
    574583                        ),
    575584                ),
    576                 'format'          => 'audio', // Post Format that maps to this
     585                'format'          => 'audio',
    577586                'description'     => __( 'listening to audio; sometimes called a scrobble', 'indieweb-post-kinds' ),
    578587                'description_url' => 'http://indieweb.org/listen',
    579588                'shortlink'       => 'x',
    580                 'title'           => false, // Should this kind have an explicit title
    581                 'show'            => true, // Show in Settings
     589                'title'           => false,
     590                'show'            => true,
    582591        )
    583592);
    … …  
    586595        'watch',
    587596        array(
    588                 'singular_name'   => __( 'Watch', 'indieweb-post-kinds' ), // Name for one instance of the kind
    589                 'name'            => __( 'Watches', 'indieweb-post-kinds' ), // General name for the kind plural
    590                 'verb'            => __( 'Watched', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    591                 'property'        => 'watch-of', // microformats 2 property
     597                'singular_name'   => __( 'Watch', 'indieweb-post-kinds' ),
     598                'name'            => __( 'Watches', 'indieweb-post-kinds' ),
     599                'verb'            => __( 'Watched', 'indieweb-post-kinds' ),
     600                'property'        => 'watch-of',
    592601                'properties'      => array(
    593602                        'watch-of' => array(
    … …  
    639648                        ),
    640649                ),
    641                 'format'          => 'video', // Post Format that maps to this
     650                'format'          => 'video',
    642651                'description'     => __( 'watching a movie, television show, online video, play or other visual-based event', 'indieweb-post-kinds' ),
    643652                'description_url' => 'http://indieweb.org/watch',
    644653                'shortlink'       => 'x',
    645                 'title'           => false, // Should this kind have an explicit title
    646                 'show'            => true, // Show in Settings
     654                'title'           => false,
     655                'show'            => true,
    647656        )
    648657);
    … …  
    651660        'checkin',
    652661        array(
    653                 'singular_name'   => __( 'Checkin', 'indieweb-post-kinds' ), // Name for one instance of the kind
    654                 'name'            => __( 'Checkins', 'indieweb-post-kinds' ), // General name for the kind plural
    655                 'verb'            => __( 'Checked into', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    656                 'property'        => 'checkin', // microformats 2 property
     662                'singular_name'   => __( 'Checkin', 'indieweb-post-kinds' ),
     663                'name'            => __( 'Checkins', 'indieweb-post-kinds' ),
     664                'verb'            => __( 'Checked into', 'indieweb-post-kinds' ),
     665                'property'        => 'checkin',
    657666                'properties'      => array(
    658667                        'checkin' => array(
    … …  
    707716                        ),
    708717                ),
    709                 'format'          => 'status', // Post Format that maps to this
     718                'format'          => 'status',
    710719                'description'     => __( 'identifying you are at a particular geographic location', 'indieweb-post-kinds' ),
    711720                'description_url' => 'http://indieweb.org/checkin',
    712721                'shortlink'       => 'g',
    713                 'title'           => false, // Should this kind have an explicit title
    714                 'show'            => true, // Show in Settings
     722                'title'           => false,
     723                'show'            => true,
    715724        )
    716725);
    … …  
    719728        'wish',
    720729        array(
    721                 'singular_name'   => __( 'Wish', 'indieweb-post-kinds' ), // Name for one instance of the kind
    722                 'name'            => __( 'Wishes', 'indieweb-post-kinds' ), // General name for the kind plural
    723                 'verb'            => __( 'Wished', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    724                 'property'        => 'wish-of', // microformats 2 property
    725                 'format'          => 'link', // Post Format that maps to this
     730                'singular_name'   => __( 'Wish', 'indieweb-post-kinds' ),
     731                'name'            => __( 'Wishes', 'indieweb-post-kinds' ),
     732                'verb'            => __( 'Wished', 'indieweb-post-kinds' ),
     733                'property'        => 'wish-of',
     734                'format'          => 'link',
    726735                'description'     => __( 'a post indicating a desire/wish. The archive of which would be a wishlist, such as a gift registry or similar', 'indieweb-post-kinds' ),
    727736                'description_url' => '',
    728737                'shortlink'       => 'f',
    729                 'title'           => false, // Should this kind have an explicit title
    730                 'show'            => false, // Show in Settings
     738                'title'           => false,
     739                'show'            => false,
    731740        )
    732741);
    … …  
    735744        'play',
    736745        array(
    737                 'singular_name'   => __( 'Play', 'indieweb-post-kinds' ), // Name for one instance of the kind
    738                 'name'            => __( 'Playing', 'indieweb-post-kinds' ), // General name for the kind plural
    739                 'verb'            => __( 'Played', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    740                 'property'        => 'play-of', // microformats 2 property
    741                 'format'          => 'status', // Post Format that maps to this
     746                'singular_name'   => __( 'Play', 'indieweb-post-kinds' ),
     747                'name'            => __( 'Playing', 'indieweb-post-kinds' ),
     748                'verb'            => __( 'Played', 'indieweb-post-kinds' ),
     749                'property'        => 'play-of',
     750                'format'          => 'status',
    742751                'description'     => __( 'playing a game', 'indieweb-post-kinds' ),
    743752                'description_url' => 'https://indieweb.org/game_play',
    744753                'shortlink'       => 'x',
    745                 'title'           => false, // Should this kind have an explicit title
    746                 'show'            => true, // Show in Settings
     754                'title'           => false,
     755                'show'            => true,
    747756        )
    748757);
    … …  
    751760        'weather',
    752761        array(
    753                 'singular_name'   => __( 'Weather', 'indieweb-post-kinds' ), // Name for one instance of the kind
    754                 'name'            => __( 'Weather', 'indieweb-post-kinds' ), // General name for the kind plural
    755                 'verb'            => ' ', // The string for the verb or action (liked this)
    756                 'property'        => 'weather', // microformats 2 property
    757                 'format'          => 'status', // Post Format that maps to this
     762                'singular_name'   => __( 'Weather', 'indieweb-post-kinds' ),
     763                'name'            => __( 'Weather', 'indieweb-post-kinds' ),
     764                'verb'            => ' ',
     765                'property'        => 'weather',
     766                'format'          => 'status',
    758767                'description'     => __( 'current weather conditions', 'indieweb-post-kinds' ),
    759768                'description_url' => 'https://indieweb.org/weather',
    760769                'shortlink'       => 'u',
    761                 'title'           => false, // Should this kind have an explicit title
    762                 'show'            => false, // Show in Settings
     770                'title'           => false,
     771                'show'            => false,
    763772        )
    764773);
    … …  
    767776        'exercise',
    768777        array(
    769                 'singular_name'   => __( 'Exercise', 'indieweb-post-kinds' ), // Name for one instance of the kind
    770                 'name'            => __( 'Exercise', 'indieweb-post-kinds' ), // General name for the kind plural
    771                 'verb'            => __( 'Exercised', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    772                 'property'        => 'exercise', // microformats 2 property
    773                 'format'          => 'status', // Post Format that maps to this
     778                'singular_name'   => __( 'Exercise', 'indieweb-post-kinds' ),
     779                'name'            => __( 'Exercise', 'indieweb-post-kinds' ),
     780                'verb'            => __( 'Exercised', 'indieweb-post-kinds' ),
     781                'property'        => 'exercise',
     782                'format'          => 'status',
    774783                'description'     => __( 'some form of physical activity or workout (examples: walk, run, cycle, hike, yoga, etc.)', 'indieweb-post-kinds' ),
    775784                'description_url' => 'https://indieweb.org/exercise',
    776785                'shortlink'       => 'm',
    777                 'title'           => false, // Should this kind have an explicit title
    778                 'show'            => false, // Show in Settings
     786                'title'           => false,
     787                'show'            => false,
    779788        )
    780789);
    … …  
    783792        'trip',
    784793        array(
    785                 'singular_name'   => __( 'Trip', 'indieweb-post-kinds' ), // Name for one instance of the kind
    786                 'name'            => __( 'Trips', 'indieweb-post-kinds' ), // General name for the kind plural
    787                 'verb'            => __( 'Travelled', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    788                 'property'        => 'trip', // microformats 2 property
    789                 'format'          => '', // Post Format that maps to this
     794                'singular_name'   => __( 'Trip', 'indieweb-post-kinds' ),
     795                'name'            => __( 'Trips', 'indieweb-post-kinds' ),
     796                'verb'            => __( 'Travelled', 'indieweb-post-kinds' ),
     797                'property'        => 'trip',
     798                'format'          => '',
    790799                'description'     => __( 'represents a geographic journey', 'indieweb-post-kinds' ),
    791800                'description_url' => 'https://indieweb.org/trip',
    792801                'shortlink'       => 'e',
    793                 'title'           => false, // Should this kind have an explicit title
    794                 'show'            => false, // Show in Settings
     802                'title'           => false,
     803                'show'            => false,
    795804        )
    796805);
    … …  
    799808        'itinerary',
    800809        array(
    801                 'singular_name'   => __( 'Itinerary', 'indieweb-post-kinds' ), // Name for one instance of the kind
    802                 'name'            => __( 'Itineraries', 'indieweb-post-kinds' ), // General name for the kind plural
    803                 'verb'            => __( 'Travelled', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    804                 'property'        => 'itinerary', // microformats 2 property
    805                 'format'          => '', // Post Format that maps to this
     810                'singular_name'   => __( 'Itinerary', 'indieweb-post-kinds' ),
     811                'name'            => __( 'Itineraries', 'indieweb-post-kinds' ),
     812                'verb'            => __( 'Travelled', 'indieweb-post-kinds' ),
     813                'property'        => 'itinerary',
     814                'format'          => '',
    806815                'description'     => __( 'parts of a scheduled trip including transit by car, plane, train, etc.', 'indieweb-post-kinds' ),
    807816                'description_url' => 'https://indieweb.org/trip',
    808817                'shortlink'       => 'e',
    809                 'title'           => false, // Should this kind have an explicit title
    810                 'show'            => false, // Show in Settings
     818                'title'           => false,
     819                'show'            => false,
    811820        )
    812821);
    … …  
    816825        'eat',
    817826        array(
    818                 'singular_name'   => __( 'Eat', 'indieweb-post-kinds' ), // Name for one instance of the kind
    819                 'name'            => __( 'Eat', 'indieweb-post-kinds' ), // General name for the kind plural
    820                 'verb'            => __( 'Ate', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    821                 'property'        => 'ate', // microformats 2 property
    822                 'format'          => 'status', // Post Format that maps to this
     827                'singular_name'   => __( 'Eat', 'indieweb-post-kinds' ),
     828                'name'            => __( 'Eat', 'indieweb-post-kinds' ),
     829                'verb'            => __( 'Ate', 'indieweb-post-kinds' ),
     830                'property'        => 'ate',
     831                'format'          => 'status',
    823832                'description'     => __( 'what you are eating, perhaps for a food diary', 'indieweb-post-kinds' ),
    824833                'description_url' => 'https://indieweb.org/food',
    825834                'shortlink'       => 'u',
    826                 'title'           => false, // Should this kind have an explicit title
    827                 'show'            => true, // Show in Settings
     835                'title'           => false,
     836                'show'            => true,
    828837        )
    829838);
    … …  
    833842        'drink',
    834843        array(
    835                 'singular_name'   => __( 'Drink', 'indieweb-post-kinds' ), // Name for one instance of the kind
    836                 'name'            => __( 'Drinks', 'indieweb-post-kinds' ), // General name for the kind plural
    837                 'verb'            => __( 'Drank', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    838                 'property'        => 'drank', // microformats 2 property
    839                 'format'          => 'status', // Post Format that maps to this
     844                'singular_name'   => __( 'Drink', 'indieweb-post-kinds' ),
     845                'name'            => __( 'Drinks', 'indieweb-post-kinds' ),
     846                'verb'            => __( 'Drank', 'indieweb-post-kinds' ),
     847                'property'        => 'drank',
     848                'format'          => 'status',
    840849                'description'     => __( 'what you are drinking, perhaps for a food dairy', 'indieweb-post-kinds' ),
    841850                'description_url' => 'https://indieweb.org/food',
    842851                'shortlink'       => 'u',
    843                 'title'           => false, // Should this kind have an explicit title
    844                 'show'            => true, // Show in Settings
     852                'title'           => false,
     853                'show'            => true,
    845854        )
    846855);
    … …  
    850859        'follow',
    851860        array(
    852                 'singular_name'   => __( 'Follow', 'indieweb-post-kinds' ), // Name for one instance of the kind
    853                 'name'            => __( 'Follows', 'indieweb-post-kinds' ), // General name for the kind plural
    854                 'verb'            => __( 'Followed', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    855                 'property'        => 'follow-of', // microformats 2 property
    856                 'format'          => '', // Post Format that maps to this
     861                'singular_name'   => __( 'Follow', 'indieweb-post-kinds' ),
     862                'name'            => __( 'Follows', 'indieweb-post-kinds' ),
     863                'verb'            => __( 'Followed', 'indieweb-post-kinds' ),
     864                'property'        => 'follow-of',
     865                'format'          => '',
    857866                'description'     => __( 'indicating you are now following or subscribing to another person`s activities online', 'indieweb-post-kinds' ),
    858867                'description_url' => 'https://indieweb.org/follow',
    859868                'shortlink'       => 'f',
    860                 'title'           => false, // Should this kind have an explicit title
    861                 'show'            => false, // Show in Settings
     869                'title'           => false,
     870                'show'            => false,
    862871        )
    863872);
    … …  
    867876        'jam',
    868877        array(
    869                 'singular_name'   => __( 'Jam', 'indieweb-post-kinds' ), // Name for one instance of the kind
    870                 'name'            => __( 'Jams', 'indieweb-post-kinds' ), // General name for the kind plural
    871                 'verb'            => __( 'Listened to', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    872                 'property'        => 'jam-of', // microformats 2 property
    873                 'format'          => 'audio', // Post Format that maps to this
     878                'singular_name'   => __( 'Jam', 'indieweb-post-kinds' ),
     879                'name'            => __( 'Jams', 'indieweb-post-kinds' ),
     880                'verb'            => __( 'Listened to', 'indieweb-post-kinds' ),
     881                'property'        => 'jam-of',
     882                'format'          => 'audio',
    874883                'description'     => __( 'a particularly personally meaningful song (a listen with added emphasis)', 'indieweb-post-kinds' ),
    875884                'description_url' => 'https://indieweb.org/jam',
    876885                'shortlink'       => 'f',
    877                 'title'           => false, // Should this kind have an explicit title
    878                 'show'            => true, // Show in Settings
     886                'title'           => false,
     887                'show'            => true,
    879888        )
    880889);
    … …  
    884893        'read',
    885894        array(
    886                 'singular_name'   => __( 'Read', 'indieweb-post-kinds' ), // Name for one instance of the kind
    887                 'name'            => __( 'Reads', 'indieweb-post-kinds' ), // General name for the kind plural
    888                 'verb'            => __( 'Read', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    889                 'property'        => 'read-of', // microformats 2 property
    890                 'format'          => 'status', // Post Format that maps to this
     895                'singular_name'   => __( 'Read', 'indieweb-post-kinds' ),
     896                'name'            => __( 'Reads', 'indieweb-post-kinds' ),
     897                'verb'            => __( 'Read', 'indieweb-post-kinds' ),
     898                'property'        => 'read-of',
     899                'format'          => 'status',
    891900                'description'     => __( 'reading a book, magazine, newspaper, other physical document, or online post', 'indieweb-post-kinds' ),
    892901                'description_url' => 'https://indieweb.org/read',
    893902                'shortlink'       => 'x',
    894                 'title'           => false, // Should this kind have an explicit title
    895                 'show'            => true, // Show in Settings
     903                'title'           => false,
     904                'show'            => true,
    896905        )
    897906);
    … …  
    901910        'quote',
    902911        array(
    903                 'singular_name'   => __( 'Quote', 'indieweb-post-kinds' ), // Name for one instance of the kind
    904                 'name'            => __( 'Quotes', 'indieweb-post-kinds' ), // General name for the kind plural
    905                 'verb'            => __( 'Quoted', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    906                 'property'        => 'quotation-of', // microformats 2 property
    907                 'format'          => 'quote', // Post Format that maps to this
     912                'singular_name'   => __( 'Quote', 'indieweb-post-kinds' ),
     913                'name'            => __( 'Quotes', 'indieweb-post-kinds' ),
     914                'verb'            => __( 'Quoted', 'indieweb-post-kinds' ),
     915                'property'        => 'quotation-of',
     916                'format'          => 'quote',
    908917                'description'     => __( 'quoted content', 'indieweb-post-kinds' ),
    909918                'description_url' => 'https://indieweb.org/quote',
    910919                'shortlink'       => 't',
    911                 'title'           => false, // Should this kind have an explicit title
    912                 'show'            => true, // Show in Settings
     920                'title'           => false,
     921                'show'            => true,
    913922        )
    914923);
    … …  
    918927        'mood',
    919928        array(
    920                 'singular_name'   => __( 'Mood', 'indieweb-post-kinds' ), // Name for one instance of the kind
    921                 'name'            => __( 'Moods', 'indieweb-post-kinds' ), // General name for the kind plural
    922                 'verb'            => __( 'Felt', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    923                 'property'        => 'mood', // microformats 2 property
    924                 'format'          => 'status', // Post Format that maps to this
     929                'singular_name'   => __( 'Mood', 'indieweb-post-kinds' ),
     930                'name'            => __( 'Moods', 'indieweb-post-kinds' ),
     931                'verb'            => __( 'Felt', 'indieweb-post-kinds' ),
     932                'property'        => 'mood',
     933                'format'          => 'status',
    925934                'description'     => __( 'how you are feeling (example: happy, sad, indifferent, etc.)', 'indieweb-post-kinds' ),
    926935                'description_url' => 'https://indieweb.org/mood',
    927936                'shortlink'       => 't',
    928                 'title'           => false, // Should this kind have an explicit title
    929                 'show'            => false, // Show in Settings
     937                'title'           => false,
     938                'show'            => false,
    930939        )
    931940);
    … …  
    935944        'recipe',
    936945        array(
    937                 'singular_name'   => __( 'Recipe', 'indieweb-post-kinds' ), // Name for one instance of the kind
    938                 'name'            => __( 'Recipes', 'indieweb-post-kinds' ), // General name for the kind plural
    939                 'verb'            => __( 'Cooked', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    940                 'property'        => 'recipe', // microformats 2 property
    941                 'format'          => '', // Post Format that maps to this
     946                'singular_name'   => __( 'Recipe', 'indieweb-post-kinds' ),
     947                'name'            => __( 'Recipes', 'indieweb-post-kinds' ),
     948                'verb'            => __( 'Cooked', 'indieweb-post-kinds' ),
     949                'property'        => 'recipe',
     950                'format'          => '',
    942951                'description'     => __( 'list of ingredients and directions for making food or drink', 'indieweb-post-kinds' ),
    943952                'description_url' => 'https://indieweb.org/recipe',
    944953                'shortlink'       => 'b',
    945                 'title'           => true, // Should this kind have an explicit title
    946                 'show'            => false, // Show in Settings
     954                'title'           => true,
     955                'show'            => false,
    947956        )
    948957);
    … …  
    952961        'issue',
    953962        array(
    954                 'singular_name'   => __( 'Issue', 'indieweb-post-kinds' ), // Name for one instance of the kind
    955                 'name'            => __( 'Issues', 'indieweb-post-kinds' ), // General name for the kind plural
    956                 'verb'            => __( 'Filed an Issue', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    957                 'property'        => 'in-reply-to', // microformats 2 property
    958                 'format'          => '', // Post Format that maps to this
     963                'singular_name'   => __( 'Issue', 'indieweb-post-kinds' ),
     964                'name'            => __( 'Issues', 'indieweb-post-kinds' ),
     965                'verb'            => __( 'Filed an Issue', 'indieweb-post-kinds' ),
     966                'property'        => 'in-reply-to',
     967                'format'          => '',
    959968                'description'     => __( 'Issue is a special kind of article post that is a reply to typically some source code, though potentially anything at a source control repository.', 'indieweb-post-kinds' ),
    960969                'description_url' => 'https://indieweb.org/issue',
    961970                'shortlink'       => 't',
    962                 'title'           => true, // Should this kind have an explicit title
    963                 'show'            => true, // Show in Settings
     971                'title'           => true,
     972                'show'            => true,
    964973        )
    965974);
    … …  
    969978        'question',
    970979        array(
    971                 'singular_name'   => __( 'Question', 'indieweb-post-kinds' ), // Name for one instance of the kind
    972                 'name'            => __( 'Questions', 'indieweb-post-kinds' ), // General name for the kind plural
    973                 'verb'            => __( 'Asked a question', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    974                 'property'        => 'question', // microformats 2 property
    975                 'format'          => '', // Post Format that maps to this
     980                'singular_name'   => __( 'Question', 'indieweb-post-kinds' ),
     981                'name'            => __( 'Questions', 'indieweb-post-kinds' ),
     982                'verb'            => __( 'Asked a question', 'indieweb-post-kinds' ),
     983                'property'        => 'question',
     984                'format'          => '',
    976985                'description'     => __( 'Question is a post type for soliciting answer replies, which are then typically up/down voted by others and then displayed underneath the question post ordered by highest positive vote count rather than time ordered.', 'indieweb-post-kinds' ),
    977986                'description_url' => 'https://indieweb.org/question',
    978987                'shortlink'       => 'q',
    979                 'title'           => false, // Should this kind have an explicit title
    980                 'show'            => false, // Show in Settings
     988                'title'           => false,
     989                'show'            => false,
    981990        )
    982991);
    … …  
    986995        'sleep',
    987996        array(
    988                 'singular_name'   => __( 'Sleep', 'indieweb-post-kinds' ), // Name for one instance of the kind
    989                 'name'            => __( 'Sleeps', 'indieweb-post-kinds' ), // General name for the kind plural
    990                 'verb'            => __( 'Slept', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    991                 'property'        => 'sleep', // microformats 2 property
    992                 'format'          => '', // Post Format that maps to this
     997                'singular_name'   => __( 'Sleep', 'indieweb-post-kinds' ),
     998                'name'            => __( 'Sleeps', 'indieweb-post-kinds' ),
     999                'verb'            => __( 'Slept', 'indieweb-post-kinds' ),
     1000                'property'        => 'sleep',
     1001                'format'          => '',
    9931002                'description'     => __( 'Sleep is a passive metrics post type that indicates how much time (and often a graph of how deeply) a person has slept.', 'indieweb-post-kinds' ),
    9941003                'description_url' => 'https://indieweb.org/sleep',
    9951004                'shortlink'       => 'm',
    996                 'title'           => false, // Should this kind have an explicit title
    997                 'show'            => false, // Show in Settings
     1005                'title'           => false,
     1006                'show'            => false,
    9981007        )
    9991008);
    … …  
    10031012        'event',
    10041013        array(
    1005                 'singular_name'   => __( 'Event', 'indieweb-post-kinds' ), // Name for one instance of the kind
    1006                 'name'            => __( 'Events', 'indieweb-post-kinds' ), // General name for the kind plural
    1007                 'verb'            => __( 'Planned', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    1008                 'property'        => 'event', // microformats 2 property
    1009                 'format'          => '', // Post Format that maps to this
     1014                'singular_name'   => __( 'Event', 'indieweb-post-kinds' ),
     1015                'name'            => __( 'Events', 'indieweb-post-kinds' ),
     1016                'verb'            => __( 'Planned', 'indieweb-post-kinds' ),
     1017                'property'        => 'event',
     1018                'format'          => '',
    10101019                'description'     => __( 'An event is a type of post that in addition to a post name (event title) has a start datetime (likely end datetime), and a location.', 'indieweb-post-kinds' ),
    10111020                'description_url' => 'https://indieweb.org/event',
    10121021                'shortlink'       => 'e',
    1013                 'title'           => true, // Should this kind have an explicit title
    1014                 'show'            => false, // Show in Settings
     1022                'title'           => true,
     1023                'show'            => false,
    10151024        )
    10161025);
    … …  
    10191028        'review',
    10201029        array(
    1021                 'singular_name'   => __( 'Review', 'indieweb-post-kinds' ), // Name for one instance of the kind
    1022                 'name'            => __( 'Reviews', 'indieweb-post-kinds' ), // General name for the kind plural
    1023                 'verb'            => __( 'Reviewed', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    1024                 'property'        => 'review-of', // microformats 2 property
    1025                 'format'          => '', // Post Format that maps to this
     1030                'singular_name'   => __( 'Review', 'indieweb-post-kinds' ),
     1031                'name'            => __( 'Reviews', 'indieweb-post-kinds' ),
     1032                'verb'            => __( 'Reviewed', 'indieweb-post-kinds' ),
     1033                'property'        => 'review-of',
     1034                'format'          => '',
    10261035                'description'     => __( 'A review is a post evaluating a product or service, usually involving a written description, sometimes with summary numerical evaluations, also known as just a rating.', 'indieweb-post-kinds' ),
    10271036                'description_url' => 'https://indieweb.org/review',
    10281037                'shortlink'       => 't',
    1029                 'title'           => false, // Should this kind have an explicit title
    1030                 'show'            => true, // Show in Settings
     1038                'title'           => false,
     1039                'show'            => true,
    10311040        )
    10321041);
    … …  
    10361045        'acquisition',
    10371046        array(
    1038                 'singular_name'   => __( 'Acquisition', 'indieweb-post-kinds' ), // Name for one instance of the kind
    1039                 'name'            => __( 'Acquisitions', 'indieweb-post-kinds' ), // General name for the kind plural
    1040                 'verb'            => __( 'Acquired', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    1041                 'property'        => 'acquired-of', // microformats 2 property
    1042                 'format'          => 'status', // Post Format that maps to this
     1047                'singular_name'   => __( 'Acquisition', 'indieweb-post-kinds' ),
     1048                'name'            => __( 'Acquisitions', 'indieweb-post-kinds' ),
     1049                'verb'            => __( 'Acquired', 'indieweb-post-kinds' ),
     1050                'property'        => 'acquired-of',
     1051                'format'          => 'status',
    10431052                'description'     => __( 'Purchases, gifts, found things, or objects donated', 'indieweb-post-kinds' ),
    10441053                'description_url' => 'http://indieweb.org/acquisition',
    10451054                'shortlink'       => 'f',
    1046                 'title'           => false, // Should this kind have an explicit title
    1047                 'show'            => false, // Show in Settings
     1055                'title'           => false,
     1056                'show'            => false,
    10481057        )
    10491058);
    … …  
    10521061        'craft',
    10531062        array(
    1054                 'singular_name'   => __( 'Craft', 'indieweb-post-kinds' ), // Name for one instance of the kind
    1055                 'name'            => __( 'Crafts', 'indieweb-post-kinds' ), // General name for the kind plural
    1056                 'verb'            => __( 'Crafted', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    1057                 'property'        => 'craft-of', // microformats 2 property
    1058                 'format'          => 'status', // Post Format that maps to this
     1063                'singular_name'   => __( 'Craft', 'indieweb-post-kinds' ),
     1064                'name'            => __( 'Crafts', 'indieweb-post-kinds' ),
     1065                'verb'            => __( 'Crafted', 'indieweb-post-kinds' ),
     1066                'property'        => 'craft-of',
     1067                'format'          => 'status',
    10591068                'description'     => __( 'Activities like knitting, crocheting, cross stitch, wood working, restoration, 3d printing...the activity of building something.', 'indieweb-post-kinds' ),
    10601069                'description_url' => 'https://indieweb.org/crafts',
    10611070                'shortlink'       => 'x',
    1062                 'title'           => false, // Should this kind have an explicit title
    1063                 'show'            => true, // Show in Settings
    1064         )
    1065 );
     1071                'title'           => false,
     1072                'show'            => true,
     1073        )
     1074);
  • indieweb-post-kinds/trunk/includes/time-functions.php

    r2973430 r3738687  
    11<?php
    22/**
    3  * Time Functions
     3 * Global date and time functions.
    44 *
    5  * Global Scoped Functions for Handling Time.
     5 * The generic helpers moved to Kind_Time in 4.0.0; their global names remain
     6 * here as deprecated wrappers.
    67 *
     8 * @package Post_Kinds
     9 * @since   3.0.0
    710 */
    811
     12defined( 'ABSPATH' ) || exit;
     13
     14if ( ! function_exists( 'kind_safe_datetime' ) ) {
     15        /**
     16         * Creates a date from a value without throwing on invalid input.
     17         *
     18         * Dates come from post meta and Micropub requests, where a malformed value
     19         * would otherwise throw an uncaught exception and break the page.
     20         *
     21         * @since 4.0.0
     22         *
     23         * @param DateTimeInterface|string $value     Date, or a date string.
     24         * @param DateTimeZone|null        $timezone  Optional. Timezone for strings without one.
     25         * @param bool                     $immutable Optional. Whether to return a DateTimeImmutable. Default true.
     26         * @return DateTimeImmutable|DateTime|false The date, or false if the value is not a valid date.
     27         */
     28        function kind_safe_datetime( $value, $timezone = null, $immutable = true ) {
     29                if ( $value instanceof DateTimeInterface ) {
     30                        if ( $immutable ) {
     31                                return $value instanceof DateTimeImmutable ? $value : DateTimeImmutable::createFromMutable( $value );
     32                        }
     33                        return $value instanceof DateTime ? $value : DateTime::createFromImmutable( $value );
     34                }
     35                if ( ! is_string( $value ) || '' === trim( $value ) ) {
     36                        return false;
     37                }
     38                try {
     39                        return $immutable ? new DateTimeImmutable( $value, $timezone ) : new DateTime( $value, $timezone );
     40                } catch ( Exception $e ) {
     41                        return false;
     42                }
     43        }
     44}
     45
     46if ( ! function_exists( 'kind_safe_interval' ) ) {
     47        /**
     48         * Creates a duration from an ISO 8601 duration without throwing on invalid input.
     49         *
     50         * @since 4.0.0
     51         *
     52         * @param DateInterval|string $value Duration, or an ISO 8601 duration such as PT3M30S.
     53         * @return DateInterval|false The duration, or false if the value is not a valid duration.
     54         */
     55        function kind_safe_interval( $value ) {
     56                if ( $value instanceof DateInterval ) {
     57                        return $value;
     58                }
     59                if ( ! is_string( $value ) || '' === trim( $value ) ) {
     60                        return false;
     61                }
     62                try {
     63                        return new DateInterval( $value );
     64                } catch ( Exception $e ) {
     65                        return false;
     66                }
     67        }
     68}
     69
    970if ( ! function_exists( 'tz_seconds_to_offset' ) ) {
     71        /**
     72         * Converts a UTC offset in seconds to an offset string.
     73         *
     74         * @since 2.3.0
     75         *
     76         * @param int $seconds Offset in seconds.
     77         * @return string Offset such as '-05:00'.
     78         */
    1079        function tz_seconds_to_offset( $seconds ) {
    1180                return ( $seconds < 0 ? '-' : '+' ) . sprintf( '%02d:%02d', abs( $seconds / 60 / 60 ), abs( $seconds / 60 ) % 60 );
    … …  
    1483
    1584if ( ! function_exists( 'tz_offset_to_seconds' ) ) {
     85        /**
     86         * Converts an offset string to a UTC offset in seconds.
     87         *
     88         * @since 2.3.0
     89         *
     90         * @param string $offset Offset such as '-05:00' or '-0500'.
     91         * @return int Offset in seconds, or 0 if the string is not an offset.
     92         */
    1693        function tz_offset_to_seconds( $offset ) {
    1794                if ( preg_match( '/([+-])(\d{2}):?(\d{2})/', $offset, $match ) ) {
    18                         $sign = ( '-' ? -1 : 1 === $match[1] );
     95                        $sign = ( '-' === $match[1] ) ? -1 : 1;
    1996                        return ( ( $match[2] * 60 * 60 ) + ( $match[3] * 60 ) ) * $sign;
    2097                } else {
    … …  
    25102
    26103if ( ! function_exists( 'tz_seconds_to_timezone' ) ) {
     104        /**
     105         * Converts a UTC offset in seconds to a time zone.
     106         *
     107         * @since 3.2.2
     108         *
     109         * @param int $seconds Offset in seconds.
     110         * @return DateTimeZone|false The time zone, or false if it cannot be created.
     111         */
    27112        function tz_seconds_to_timezone( $seconds ) {
    28                 if ( version_compare( phpversion(), '5.5.10', '<' ) ) {
    29                         return timezone_name_from_abbr( '', $seconds, 0 );
    30                 }
    31113                if ( 0 !== $seconds ) {
    32114                        $tz = timezone_open( tz_seconds_to_offset( $seconds ) );
    … …  
    40122
    41123if ( ! function_exists( 'tz_timezone_to_seconds' ) ) {
     124        /**
     125         * Returns a time zone's UTC offset in seconds.
     126         *
     127         * @since 3.2.2
     128         *
     129         * @param string $timezone Time zone name or offset string.
     130         * @return int|false Offset in seconds, or false if the time zone is not valid.
     131         */
    42132        function tz_timezone_to_seconds( $timezone ) {
    43133                $tz = timezone_open( $timezone );
    … …  
    51141if ( ! function_exists( 'get_gmt_offsets' ) ) {
    52142
     143        /**
     144         * Returns the UTC offsets currently in use by any time zone, sorted.
     145         *
     146         * The list is built from every time zone, so it is kept in the object
     147         * cache for the day: for the request, and across requests when the cache
     148         * is persistent. Daylight saving changes it at most a few times a year.
     149         *
     150         * @since 3.0.0
     151         *
     152         * @return string[] Offsets such as '-05:00'.
     153         */
    53154        function get_gmt_offsets() {
     155                $key = 'gmt_offsets_' . gmdate( 'Y-m-d' );
     156                $o   = wp_cache_get( $key, 'post-kinds' );
     157                if ( is_array( $o ) ) {
     158                        return $o;
     159                }
    54160                $o       = array();
    55161                $t_zones = timezone_identifiers_list();
    56162                foreach ( $t_zones as $a ) {
    57                         $t = '';
    58163                        try {
    59                                 // this throws exception for 'US/Pacific-New'
     164                                // Some identifiers, such as 'US/Pacific-New', throw on some PHP versions.
    60165                                $zone    = new DateTimeZone( $a );
    61166                                $seconds = $zone->getOffset( new DateTime( 'now', $zone ) );
    62167                                $o[]     = tz_seconds_to_offset( $seconds );
    63168                        } catch ( Exception $e ) {
    64                                 die( 'Exception : ' . esc_html( $e->getMessage() ) . '<br />' );
    65                                 // what to do in catch ? , nothing just relax
     169                                // Skip the zone rather than fail.
     170                                continue;
    66171                        }
    67172                }
    68173                $o = array_unique( $o );
    69174                asort( $o );
     175                wp_cache_set( $key, $o, 'post-kinds', DAY_IN_SECONDS );
    70176                return $o;
    71177        }
    … …  
    73179
    74180if ( ! function_exists( 'get_default_offset' ) ) {
    75         // Gets the default offset
     181        /**
     182         * Returns the site's UTC offset, from its gmt_offset option.
     183         *
     184         * @since 3.0.0
     185         *
     186         * @return string Offset such as '-05:00'.
     187         */
    76188        function get_default_offset() {
    77189                $tz_seconds = get_option( 'gmt_offset' ) * 3600;
    … …  
    81193
    82194if ( ! function_exists( 'build_iso8601_time' ) ) {
    83         // Turns individual pieces of a date and time into a single ISO8601 string
     195        /**
     196         * Joins a date, time and offset into an ISO 8601 string.
     197         *
     198         * @since 3.0.0
     199         *
     200         * @param string $date   Date in Y-m-d format.
     201         * @param string $time   Time in H:i:s format.
     202         * @param string $offset Offset such as '-05:00'. Empty for the site's offset.
     203         * @return string The date and time, or an empty string if both are empty.
     204         */
    84205        function build_iso8601_time( $date, $time, $offset ) {
    85206                if ( empty( $date ) && empty( $time ) ) {
    … …  
    94215
    95216if ( ! function_exists( 'build_iso8601_duration' ) ) {
    96         // Given an array with the pieces of a duration build an ISO8601 duration
     217        /**
     218         * Builds an ISO 8601 duration from its pieces.
     219         *
     220         * @since 3.0.0
     221         *
     222         * @param int[] $values Years (Y), months (M), days (D), hours (H), minutes (I) and seconds (S).
     223         * @return string The duration, or an empty string if every piece is empty.
     224         */
    97225        function build_iso8601_duration( $values ) {
    98226                $values = array_filter( $values );
    … …  
    128256
    129257if ( ! function_exists( 'calculate_duration' ) ) {
    130         // Given two ISO8601 time strings return a DateInterval Object
     258        /**
     259         * Returns the duration between two dates.
     260         *
     261         * @since 2.5.0
     262         * @since 4.0.0 Also accepts DateTimeInterface objects, such as Kind_Post::get( 'start' ) returns.
     263         *
     264         * @param DateTimeInterface|string $start_string Start, as a date or a string in Y-m-d\TH:i:sP format.
     265         * @param DateTimeInterface|string $end_string   End, as a date or a string in Y-m-d\TH:i:sP format.
     266         * @return DateInterval|false The duration, or false if either date is not valid or they are the same.
     267         */
    131268        function calculate_duration( $start_string, $end_string ) {
    132                 $start = array();
    133                 $end   = array();
    134                 if ( ! is_string( $start_string ) || ! is_string( $end_string ) ) {
     269                $dates = array();
     270                foreach ( array( $start_string, $end_string ) as $value ) {
     271                        if ( is_string( $value ) ) {
     272                                // Only the full format: looser parsing reads stray text, such as a single letter, as a date.
     273                                $value = date_create_immutable_from_format( 'Y-m-d\TH:i:sP', $value );
     274                        }
     275                        if ( ! $value instanceof DateTimeInterface ) {
     276                                return false;
     277                        }
     278                        $dates[] = $value;
     279                }
     280                list( $start, $end ) = $dates;
     281                if ( $start == $end ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Compares the moments, not the objects.
    135282                        return false;
    136283                }
    137                 if ( $start_string === $end_string ) {
    138                         return false;
    139                 }
    140                 $start = date_create_from_format( 'Y-m-d\TH:i:sP', $start_string );
    141                 $end   = date_create_from_format( 'Y-m-d\TH:i:sP', $end_string );
    142                 if ( ( $start instanceof DateTime ) && ( $end instanceof DateTime ) ) {
    143                         $duration = $start->diff( $end );
    144                         return $duration;
    145                 }
    146                 return false;
    147         }
    148 }
    149 
    150 if ( ! function_exists( 'seconds_to_iso8601' ) ) {
    151         function seconds_to_iso8601( $second ) {
    152                 $h   = intval( $second / 3600 );
    153                 $m   = intval( ( $second - $h * 3600 ) / 60 );
    154                 $s   = $second - ( $h * 3600 + $m * 60 );
    155                 $ret = 'PT';
    156                 if ( $h ) {
    157                         $ret .= $h . 'H';
    158                 }
    159                 if ( $m ) {
    160                         $ret .= $m . 'M';
    161                 }
    162                 if ( ( ! $h && ! $m ) || $s ) {
    163                         $ret .= $s . 'S';
    164                 }
    165                 return $ret;
     284                return $start->diff( $end );
    166285        }
    167286}
    168287
    169288if ( ! function_exists( 'date_interval_to_iso8601' ) ) {
    170 
    171         // Return a date interval as an ISO8601 string
     289        /**
     290         * Converts a duration to an ISO 8601 duration string.
     291         *
     292         * @since 3.0.0
     293         *
     294         * @param \DateInterval $interval The duration.
     295         * @return string The duration, such as 'PT3M30S'.
     296         */
    172297        function date_interval_to_iso8601( \DateInterval $interval ) {
    173298                // Reading all non-zero date parts.
    … …  
    202327}
    203328
    204 function display_formatted_datetime( $date ) {
    205         if ( is_string( $date ) ) {
    206                 $date = new DateTimeImmutable( $date );
    207         }
    208 
    209         return $date->format( get_option( 'date_format' ) ) . ' ' . $date->format( get_option( 'time_format' ) );
    210 }
    211 
    212 
    213 /**
    214  * Function to divide a datetime into an array for use in a field
    215  *
    216  * @access public
    217  *
    218  * @param DateTime $datetime
    219  * @return array {
    220  *  @type string $date Date in Y-m-d format.
    221  *  @type string $time Time in H:i:s format.
    222  *  @type DateTimeZone $timezone Timezone object.
    223  * }
    224  */
    225 function divide_datetime( $datetime ) {
    226         if ( ! $datetime ) {
    227                 return false;
    228         }
    229 
    230         if ( is_string( $datetime ) ) {
    231                 $datetime = new DateTime( $datetime );
    232         }
    233 
    234         if ( ! $datetime ) {
    235                 return false;
    236         }
    237 
    238         $time         = array();
    239         $time['date'] = $datetime->format( 'Y-m-d' );
    240         if ( '0000-01-01' === $time['date'] ) {
    241                 $time['date'] = '';
    242         }
    243         $time['time']   = $datetime->format( 'H:i:s' );
    244         $time['offset'] = get_datetime_offset( $datetime );
    245         $time['class']  = get_class( $datetime );
    246         return array_filter( $time );
    247 }
    248 
    249 /**
    250  * Function to build a datetime from individual pieces
    251  *
    252  * @access public
    253  *
    254  * @param string $date Date in Y-m-d format.
    255  * @param string $time Time in H:i:s format.
    256  * @param DateTimeZone $timezone Timezone object.
    257  *
    258  * @return DateTimeImmutable|false DateTime object or false if not valid
    259  */
    260 function build_datetime( $date, $time, $offset = null ) {
    261         if ( empty( $date ) && empty( $time ) ) {
    262                 return false;
    263         }
    264         if ( is_string( $offset ) ) {
    265                 $timezone = timezone_open( $offset );
    266         } elseif ( $offset instanceof DateTimeZone ) {
    267                 $timezone = $offset;
    268         } else {
    269                 return false;
    270         }
    271         if ( ! $timezone ) {
    272                 $timezone = wp_timezone();
    273         }
    274         return date_create_immutable_from_format( 'Y-m-d\TH:i:sP', $date . 'T' . $time, $timezone );
    275 }
    276 
    277 /**
    278  * Return a formatted offset from a datetime object
    279  *
    280  * @access public
    281  *
    282  * @param DateTime $datetime DateTime object or if not passed set to now and site timezone
    283  *
    284  * @return string|false Formatted offset or false if not valid
    285  */
    286 function get_datetime_offset( $datetime = null ) {
    287         if ( ! $datetime ) {
    288                 $datetime = new DateTimeImmutable( 'now', wp_timezone() );
    289         }
    290         $seconds = $datetime->getOffset();
    291         if ( false === $seconds ) {
    292                 return false;
    293         }
    294         return ( $seconds < 0 ? '-' : '+' ) . sprintf( '%02d:%02d', abs( $seconds / 60 / 60 ), abs( $seconds / 60 ) % 60 );
    295 }
    296 
    297 
    298 // Given an ISO8601 duration return an array with the piece otherwise 0 duration.
    299 function divide_interval( $interval ) {
    300         $default = array(
    301                 'Y' => 0,
    302                 'M' => 0,
    303                 'D' => 0,
    304                 'H' => 0,
    305                 'I' => 0,
    306                 'S' => 0,
    307         );
    308         if ( ! $interval ) {
    309                 return $default;
    310         }
    311         if ( is_string( $interval ) && ! empty( $interval ) ) {
    312                 try {
    313                         $interval = new DateInterval( $interval );
    314                 } catch ( \Exception $e ) {
    315                         return $default;
    316                 }
    317         }
    318         // Reading all non-zero date parts.
    319         $return = array(
    320                 'Y' => $interval->y,
    321                 'M' => $interval->m,
    322                 'D' => $interval->d,
    323                 'H' => $interval->h,
    324                 'I' => $interval->i,
    325                 'S' => $interval->s,
    326         );
    327         return wp_parse_args( $return, $default );
    328 }
    329 
    330 
    331 // Given an array with the pieces of a duration build an ISO8601 duration
    332 function build_interval( $values ) {
    333         $date = wp_array_slice_assoc( $values, array( 'Y', 'M', 'D' ) );
    334         $time = wp_array_slice_assoc( $values, array( 'H', 'I', 'S' ) );
    335         if ( ! $date || ! $time ) {
    336                 return '';
    337         }
    338         $spec = 'P';
    339         // Adding each part to the spec-string.
    340         foreach ( $date as $key => $value ) {
    341                 $spec .= $value . $key;
    342         }
    343         if ( count( $time ) > 0 ) {
    344                 $spec .= 'T';
    345                 foreach ( $time as $key => $value ) {
    346                         if ( 'I' === $key ) {
    347                                 $spec .= $value . 'M';
    348                         } else {
    349                                 $spec .= $value . $key;
    350                         }
    351                 }
    352         }
    353         return $spec;
    354 }
     329if ( ! function_exists( 'display_formatted_datetime' ) ) {
     330        /**
     331         * Deprecated wrapper; see Kind_Time::display_formatted_datetime().
     332         *
     333         * @since 3.2.0
     334         * @deprecated 4.0.0 Use Kind_Time::display_formatted_datetime(). Will be removed in 5.0.0.
     335         *
     336         * @param DateTimeInterface|string $date Date, or a string kind_safe_datetime() accepts.
     337         * @return string The formatted date and time, or an empty string if the date is not valid.
     338         */
     339        function display_formatted_datetime( $date ) {
     340                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::display_formatted_datetime()' );
     341                return Kind_Time::display_formatted_datetime( $date );
     342        }
     343}
     344
     345if ( ! function_exists( 'divide_datetime' ) ) {
     346        /**
     347         * Deprecated wrapper; see Kind_Time::divide_datetime().
     348         *
     349         * @since 3.3.4
     350         * @deprecated 4.0.0 Use Kind_Time::divide_datetime(). Will be removed in 5.0.0.
     351         *
     352         * @param DateTimeInterface|string $datetime Date, or a string kind_safe_datetime() accepts.
     353         * @return array|false The date's parts. See Kind_Time::divide_datetime().
     354         */
     355        function divide_datetime( $datetime ) {
     356                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::divide_datetime()' );
     357                return Kind_Time::divide_datetime( $datetime );
     358        }
     359}
     360
     361if ( ! function_exists( 'build_datetime' ) ) {
     362        /**
     363         * Deprecated wrapper; see Kind_Time::build_datetime().
     364         *
     365         * @since 3.3.4
     366         * @deprecated 4.0.0 Use Kind_Time::build_datetime(). Will be removed in 5.0.0.
     367         *
     368         * @param string                   $date   Date in Y-m-d format.
     369         * @param string                   $time   Time in H:i or H:i:s format.
     370         * @param string|DateTimeZone|null $offset Optional. Offset or time zone. Default the site's time zone.
     371         * @return DateTimeImmutable|false The date, or false if it is not valid.
     372         */
     373        function build_datetime( $date, $time, $offset = null ) {
     374                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::build_datetime()' );
     375                return Kind_Time::build_datetime( $date, $time, $offset );
     376        }
     377}
     378
     379if ( ! function_exists( 'get_datetime_offset' ) ) {
     380        /**
     381         * Deprecated wrapper; see Kind_Time::get_datetime_offset().
     382         *
     383         * @since 3.3.4
     384         * @deprecated 4.0.0 Use Kind_Time::get_datetime_offset(). Will be removed in 5.0.0.
     385         *
     386         * @param DateTimeInterface|null $datetime Optional. Date. Default now, in the site's time zone.
     387         * @return string|false The offset, or false if it cannot be read.
     388         */
     389        function get_datetime_offset( $datetime = null ) {
     390                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::get_datetime_offset()' );
     391                return Kind_Time::get_datetime_offset( $datetime );
     392        }
     393}
     394
     395if ( ! function_exists( 'divide_interval' ) ) {
     396        /**
     397         * Deprecated wrapper; see Kind_Time::divide_interval().
     398         *
     399         * @since 3.3.4
     400         * @deprecated 4.0.0 Use Kind_Time::divide_interval(). Will be removed in 5.0.0.
     401         *
     402         * @param DateInterval|string|null $interval Duration, or an ISO 8601 duration string.
     403         * @return int[] The duration's parts. See Kind_Time::divide_interval().
     404         */
     405        function divide_interval( $interval ) {
     406                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::divide_interval()' );
     407                return Kind_Time::divide_interval( $interval );
     408        }
     409}
     410
     411if ( ! function_exists( 'build_interval' ) ) {
     412        /**
     413         * Deprecated wrapper; see Kind_Time::build_interval().
     414         *
     415         * @since 3.3.4
     416         * @deprecated 4.0.0 Use Kind_Time::build_interval(). Will be removed in 5.0.0.
     417         *
     418         * @param int[] $values Years (Y), months (M), days (D), hours (H), minutes (I) and seconds (S).
     419         * @return string The duration, or an empty string if every piece is 0.
     420         */
     421        function build_interval( $values ) {
     422                _deprecated_function( __FUNCTION__, '4.0.0', 'Kind_Time::build_interval()' );
     423                return Kind_Time::build_interval( $values );
     424        }
     425}
  • indieweb-post-kinds/trunk/indieweb-post-kinds.php

    r3067251 r3738687  
    44 *
    55 * @link    http://indieweb.org/Post_Kinds_Plugin
    6  * @package Post Kinds
     6 * @package Post_Kinds
    77 * Plugin Name: Post Kinds
    88 * Plugin URI: https://wordpress.org/plugins/indieweb-post-kinds/
    99 * Description: Ever want to reply to someone else's post with a post on your own site? Or to "like" someone else's post, but with your own site?
    10  * Version: 3.7.3
     10 * Version: 4.0.0
     11 * Requires at least: 6.2
     12 * Requires PHP: 7.4
     13 * License: GPLv2 or later
     14 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1115 * Author: David Shanske
    1216 * Author URI: https://david.shanske.com
    … …  
    1418 * Domain Path:  /languages
    1519 */
     20
     21defined( 'ABSPATH' ) || exit;
    1622
    1723if ( ! defined( 'POST_KINDS_KSES' ) ) {
    … …  
    4046add_action( 'upgrader_process_complete', array( 'Post_Kinds_Plugin', 'upgrader_process_complete' ), 10, 2 );
    4147
    42 if ( ! file_exists( plugin_dir_path( __FILE__ ) . 'lib/parse-this/parse-this.php' ) ) {
    43         add_action( 'admin_notices', array( 'Post_Kinds_Plugin', 'parse_this_error' ) );
    44 }
    4548
    4649if ( Post_Kinds_Plugin::show_editor_error() ) {
    … …  
    5154add_action( 'init', array( 'Post_Kinds_Plugin', 'init' ) );
    5255
     56/**
     57 * Loads the plugin, its assets and its global functions.
     58 */
    5359class Post_Kinds_Plugin {
     60        /**
     61         * Plugin version, read from the plugin header on init.
     62         *
     63         * @since 2.4.0
     64         * @var string
     65         */
    5466        public static $version;
    5567
     68        /**
     69         * Sets the version and registers the kind taxonomy.
     70         *
     71         * @since 2.4.0
     72         */
    5673        public static function init() {
    5774                self::$version = get_file_data( __FILE__, array( 'Version' => 'Version' ) )['Version'];
    … …  
    6178        }
    6279
     80        /**
     81         * Outputs an admin notice when Parse This is not available at all.
     82         *
     83         * @since 3.1.0
     84         */
    6385        public static function parse_this_error() {
    64                 $class   = 'notice notice-error';
    65                 $message = __( 'Parse This is not installed. Please advise the developer', 'indieweb-post-kinds' );
    66                 printf( '<div class="%1$s"><p>%2$s</p></div>', esc_attr( $class ), esc_html( $message ) );
    67         }
    68 
     86                wp_admin_notice(
     87                        esc_html__( 'Parse This is not installed. Please advise the developer', 'indieweb-post-kinds' ),
     88                        array( 'type' => 'error' )
     89                );
     90        }
     91
     92        /**
     93         * Outputs an admin notice when another plugin loaded a copy of Parse This
     94         * older than 2.0.0.
     95         *
     96         * Post Kinds still works, because its bundled 2.0 API is loaded alongside the
     97         * older copy, but the older copy serves the parse-this REST route used by the
     98         * reply metabox.
     99         *
     100         * @since 4.0.0
     101         */
     102        public static function parse_this_outdated_notice() {
     103                if ( ! current_user_can( 'activate_plugins' ) ) {
     104                        return;
     105                }
     106                wp_admin_notice(
     107                        sprintf(
     108                                /* translators: %s: Name of the plugin, or path of the file, that loaded the older copy of Parse This. */
     109                                esc_html__( 'Post Kinds: %s is loading a copy of Parse This older than version 2.0. Post Kinds will keep working, but URL lookups in the post editor use that older copy. Updating that plugin, or installing the Parse This plugin (version 2.0 or later), resolves this.', 'indieweb-post-kinds' ),
     110                                '<strong>' . esc_html( self::parse_this_source() ) . '</strong>'
     111                        ),
     112                        array( 'type' => 'warning' )
     113                );
     114        }
     115
     116        /**
     117         * Returns whether the loaded copy of Parse This is version 2.0.0 or later.
     118         *
     119         * Copies older than 2.0.0 do not define PARSE_THIS_VERSION.
     120         *
     121         * @since 4.0.0
     122         *
     123         * @return bool
     124         */
     125        public static function parse_this_is_current() {
     126                return defined( 'PARSE_THIS_VERSION' ) && version_compare( PARSE_THIS_VERSION, '2.0.0', '>=' );
     127        }
     128
     129        /**
     130         * Returns a name for whatever loaded Parse This: the plugin's name, or the
     131         * file's path relative to the content directory if it isn't a plugin.
     132         *
     133         * @since 4.0.0
     134         *
     135         * @return string
     136         */
     137        public static function parse_this_source() {
     138                if ( ! function_exists( 'parse_this_loader' ) ) {
     139                        return '';
     140                }
     141                $file = wp_normalize_path( ( new ReflectionFunction( 'parse_this_loader' ) )->getFileName() );
     142                $dir  = trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) );
     143                if ( 0 === strpos( $file, $dir ) ) {
     144                        $slug = strtok( substr( $file, strlen( $dir ) ), '/' );
     145                        if ( ! function_exists( 'get_plugins' ) ) {
     146                                require_once ABSPATH . 'wp-admin/includes/plugin.php';
     147                        }
     148                        foreach ( get_plugins() as $basename => $data ) {
     149                                if ( 0 === strpos( $basename, $slug . '/' ) ) {
     150                                        return $data['Name'];
     151                                }
     152                        }
     153                        return $slug;
     154                }
     155                return str_replace( trailingslashit( wp_normalize_path( WP_CONTENT_DIR ) ), '', $file );
     156        }
     157
     158        /**
     159         * Whether to check for the block editor and warn about it.
     160         *
     161         * Not on ClassicPress, or when the Classic Editor plugin is active.
     162         *
     163         * @since 3.3.4
     164         *
     165         * @return bool
     166         */
    69167        public static function show_editor_error() {
    70                 // Do not show this error in ClassicPress
     168                // Do not show this error in ClassicPress.
    71169                if ( self::is_classicpress() ) {
    72170                        return false;
    73171                }
    74                 // Do not show if less than Version 5
    75                 if ( version_compare( get_bloginfo( 'version' ), '5.0' ) <= 0 ) {
    76                         return false;
    77                 }
    78172                if ( class_exists( 'Classic_Editor' ) ) {
    79173                        return false;
    … …  
    82176        }
    83177
     178        /**
     179         * Whether the site runs ClassicPress.
     180         *
     181         * @since 3.6.0
     182         *
     183         * @return bool
     184         */
    84185        public static function is_classicpress() {
    85186                return function_exists( 'classicpress_version' );
    86187        }
    87188
     189        /**
     190         * Outputs an admin notice on screens that use the block editor.
     191         *
     192         * Post Kinds works only with the Classic Editor. The notice is not shown
     193         * if the Classic Editor or Disable Gutenberg plugin is active.
     194         *
     195         * @since 3.2.6
     196         *
     197         * @return string|void An empty string if no notice is needed.
     198         */
    88199        public static function classic_editor_error() {
    89200                if ( ! self::post_uses_gutenberg() ) {
    … …  
    101212
    102213
     214        /**
     215         * Flushes the rewrite rules after Post Kinds is updated.
     216         *
     217         * @since 3.4.19
     218         *
     219         * @param WP_Upgrader $upgrade_object The upgrader.
     220         * @param array       $options        Details of the update.
     221         */
    103222        public static function upgrader_process_complete( $upgrade_object, $options ) {
    104223                $current_plugin_path_name = plugin_basename( __FILE__ );
    … …  
    112231        }
    113232
     233        /**
     234         * Creates the kind terms and flushes the rewrite rules on activation.
     235         *
     236         * @since 3.3.0
     237         */
    114238        public static function activate() {
    115239                Kind_Taxonomy::activate_kinds();
    … …  
    117241        }
    118242
     243        /**
     244         * Flushes the rewrite rules on deactivation.
     245         *
     246         * @since 3.3.0
     247         */
    119248        public static function deactivate() {
    120249                flush_rewrite_rules();
    121250        }
    122251
     252        /**
     253         * Loads the translations, global functions and Parse This, and adds the hooks.
     254         *
     255         * @since 2.5.2
     256         */
    123257        public static function plugins_loaded() {
    124258                $cls = get_called_class();
    125259                load_plugin_textdomain( 'indieweb-post-kinds', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
    126260
    127                 // Add WordPress Compatibility File for Functions Introduced Post 4.9.9.
     261                // Polyfills for functions newer than the plugin's minimums.
    128262                require_once plugin_dir_path( __FILE__ ) . 'includes/compat.php';
    129                 // Parse this Compat File.
    130                 require_once plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/compat-functions.php';
    131263
    132264                // Add Kind Global Functions.
    … …  
    136268                require_once plugin_dir_path( __FILE__ ) . '/includes/time-functions.php';
    137269
    138                 // Parse This
    139                 require_once plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/autoload.php';
    140                 if ( ! class_exists( 'REST_Parse_This' ) ) {
    141                         require_once plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/class-rest-parse-this.php';
    142                 }
    143                 require_once plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/functions.php';
     270                /*
     271                 * Parse This. The standalone plugin, if active, has already loaded at
     272                 * plugins_loaded priority 9 and defined parse_this_loader(). Otherwise
     273                 * load the bundled copy. Its loader sets up the classes, the functions
     274                 * and the REST route.
     275                 */
     276                $parse_this = plugin_dir_path( __FILE__ ) . 'lib/parse-this/parse-this.php';
     277                if ( ! function_exists( 'parse_this_loader' ) && file_exists( $parse_this ) ) {
     278                        require_once $parse_this;
     279                        parse_this_loader();
     280                }
     281
     282                if ( ! function_exists( 'parse_this_loader' ) ) {
     283                        add_action( 'admin_notices', array( $cls, 'parse_this_error' ) );
     284                } elseif ( ! self::parse_this_is_current() ) {
     285                        /*
     286                         * Another plugin loaded a copy older than 2.0.0. Load the bundled 2.0
     287                         * API alongside it: everything in 2.0 is in the ParseThis namespace,
     288                         * so it does not clash with the old copy's global names, and both
     289                         * versions only load their bundled Mf2\Parser and Masterminds\HTML5
     290                         * if those classes do not already exist.
     291                         */
     292                        $includes = plugin_dir_path( __FILE__ ) . 'lib/parse-this/includes/';
     293                        if ( file_exists( $includes . 'autoload.php' ) ) {
     294                                require_once $includes . 'autoload.php';
     295                                require_once $includes . 'functions.php';
     296                        }
     297                        add_action( 'admin_notices', array( $cls, 'parse_this_outdated_notice' ) );
     298                }
    144299                $class_load = array(
    145                         'Plugins', // Plugin Specific Customization
    146                         'Media_Metadata', // Media Metadata Enhancements
    147                         'Config', // Configuration Menu
    148                         'Metabox', // Metabox for Classic Editor
    149                         'View', // Kind Display Functionality
     300                        'Plugins', // Plugin Specific Customization.
     301                        'Media_Metadata', // Media Metadata Enhancements.
     302                        'Config', // Configuration Menu.
     303                        'Metabox', // Metabox for Classic Editor.
     304                        'View', // Kind Display Functionality.
     305                        'Upgrade', // One-time upgrade of stored data.
    150306                );
    151307
    152308                foreach ( $class_load as $load ) {
    153309                        add_action( 'init', array( 'Kind_' . $load, 'init' ) );
     310                }
     311
     312                if ( defined( 'WP_CLI' ) && WP_CLI ) {
     313                        WP_CLI::add_command( 'post-kinds upgrade', array( 'Kind_Upgrade', 'cli' ) );
    154314                }
    155315
    … …  
    162322                add_action( 'admin_enqueue_scripts', array( $cls, 'admin_style_load' ) );
    163323
    164                 // Load Privacy Declaration
     324                // Load Privacy Declaration.
    165325                add_action( 'admin_init', array( $cls, 'privacy_declaration' ) );
    166                 remove_all_actions( 'do_feed_rss2' );
    167                 remove_all_actions( 'do_feed_atom' );
    168                 add_action( 'do_feed_rss2', array( $cls, 'do_feed_rss2' ), 10, 1 );
    169                 add_action( 'do_feed_atom', array( $cls, 'do_feed_atom' ), 10, 1 );
    170 
    171                 // Register Widgets
     326
     327                // Register Widgets.
    172328                add_action(
    173329                        'widgets_init',
    … …  
    179335        }
    180336
    181         public static function do_feed_atom( $for_comments ) {
    182                 if ( $for_comments ) {
    183                         load_template( plugin_dir_path( __FILE__ ) . 'templates/feed-atom-comments.php' );
    184                 } else {
    185                         load_template( plugin_dir_path( __FILE__ ) . 'templates/feed-atom.php' );
    186                 }
    187         }
    188 
    189         public static function do_feed_rss2( $for_comments ) {
    190                 if ( $for_comments ) {
    191                         load_template( plugin_dir_path( __FILE__ ) . 'templates/feed-rss2-comments.php' );
    192                 } else {
    193                         load_template( plugin_dir_path( __FILE__ ) . 'templates/feed-rss2.php' );
    194                 }
    195         }
    196 
    197337        /**
    198338         * Adds link to Plugin Page for Options Page.
     339         *
     340         * @since 2.4.0
    199341         *
    200342         * @access public
    … …  
    217359        /**
    218360         * Loads the Stylesheet for the Plugin.
     361         *
     362         * @since 2.4.0
    219363         */
    220364        public static function style_load() {
    … …  
    224368        /**
    225369         * Loads the Admin Stylesheet for the Plugin.
     370         *
     371         * @since 2.4.0
    226372         */
    227373        public static function admin_style_load() {
    … …  
    229375        }
    230376
     377        /**
     378         * Adds suggested text to the privacy policy guide.
     379         *
     380         * @since 3.0.5
     381         */
    231382        public static function privacy_declaration() {
    232383                if ( function_exists( 'wp_add_privacy_policy_content' ) ) {
    … …  
    242393        }
    243394
     395        /**
     396         * Whether the current screen uses the block editor.
     397         *
     398         * Screens other than the post editor count as using it.
     399         *
     400         * @since 3.3.2
     401         *
     402         * @return bool
     403         */
    244404        public static function post_uses_gutenberg() {
    245405                $screen = get_current_screen();
  • indieweb-post-kinds/trunk/js/clone-media-fragment.js

    r2110745 r3738687  
    11function cloneMediaFragment() {
    2   // Check that the fragment is a Media Fragment (starts with t=)
    3   if(window.location.hash && window.location.hash.match(/^#t=/)) {
    4     // Find any video and audio tags on the page
    5     document.querySelectorAll("video,audio").forEach(function(el){
    6       // Create a virtual element to use as a URI parser
    7       var parser = document.createElement('a');
    8       parser.href = el.currentSrc;
    9       // Replace the hash
    10       parser.hash = window.location.hash;
    11       // Set the src of the video/audio tag to the full URL
    12       el.src = parser.href;
    13     });
    14   }
     2        // Check that the fragment is a Media Fragment (starts with t=)
     3        if ( window.location.hash && window.location.hash.match( /^#t=/ ) ) {
     4                // Find any video and audio tags on the page
     5                document.querySelectorAll( 'video,audio' ).forEach( function( el ) {
     6                        // Create a virtual element to use as a URI parser
     7                        var parser = document.createElement( 'a' );
     8                        parser.href = el.currentSrc;
     9                        // Replace the hash
     10                        parser.hash = window.location.hash;
     11                        // Set the src of the video/audio tag to the full URL
     12                        el.src = parser.href;
     13                } );
     14        }
    1515}
    1616
    17 document.addEventListener("DOMContentLoaded", function() {
    18   cloneMediaFragment();
    19   // When the media is paused, update the fragment of the page
    20   document.querySelectorAll("video,audio").forEach(function(el) {
    21     el.addEventListener("pause", function(event) {
    22       // Update the media fragment to the current time
    23       // Use replaceState to avoid triggering the "hashchange" listener above
    24       history.replaceState(null, null, "#t=" + Math.round(event.target.currentTime));
    25     });
    26   });
    27 });
     17document.addEventListener( 'DOMContentLoaded', function() {
     18        cloneMediaFragment();
     19        // When the media is paused, update the fragment of the page
     20        document.querySelectorAll( 'video,audio' ).forEach( function( el ) {
     21                el.addEventListener( 'pause', function( event ) {
     22                        // Update the media fragment to the current time
     23                        // Use replaceState to avoid triggering the "hashchange" listener above
     24                        history.replaceState( null, null, '#t=' + Math.round( event.target.currentTime ) );
     25                } );
     26        } );
     27} );
    2828
    2929// If the user changes the hash manually, clone the fragment to the media URLs
    30 window.addEventListener("hashchange", cloneMediaFragment);
     30window.addEventListener( 'hashchange', cloneMediaFragment );
  • indieweb-post-kinds/trunk/js/kind.js

    r3001401 r3738687  
     1/* global PKAPI, moment, wp */
     2/* eslint-disable no-alert -- The metabox uses alert() and confirm() for feedback by design. */
    13jQuery( document ).ready( function( $ ) {
     4        var KindWindow;
     5
    26        changeSettings();
    37
    4 
    5 function clearPostProperties() {
    6         var fieldIds = [
    7                 'cite_url',
    8                 'cite_name',
    9                 'cite_summary',
    10                 'cite_tags',
    11                 'cite_media',
    12                 'cite_author_name',
    13                 'cite_author_url',
    14                 'cite_author_photo',
    15                 'cite_featured',
    16                 'cite_publication',
    17                 'mf2_rsvp',
    18                 'mf2_rating',
    19                 'mf2_start_time',
    20                 'mf2_start_date',
    21                 'mf2_end_time',
    22                 'mf2_end_date',
    23                 'cite_published_time',
    24                 'cite_published_date',
    25                 'cite_updated_time',
    26                 'cite_updated_date',
    27                 'duration_years',
    28                 'duration_months',
    29                 'duration_days',
    30                 'duration_hours',
    31                 'duration_minutes',
    32                 'duration_seconds'
    33         ];
     8        function clearPostProperties() {
     9                var fieldIds = [
     10                        'cite_url',
     11                        'cite_name',
     12                        'cite_summary',
     13                        'cite_tags',
     14                        'cite_media',
     15                        'cite_author_name',
     16                        'cite_author_url',
     17                        'cite_author_photo',
     18                        'cite_featured',
     19                        'cite_publication',
     20                        'mf2_rsvp',
     21                        'mf2_rating',
     22                        'mf2_start_time',
     23                        'mf2_start_date',
     24                        'mf2_end_time',
     25                        'mf2_end_date',
     26                        'cite_published_time',
     27                        'cite_published_date',
     28                        'cite_updated_time',
     29                        'cite_updated_date',
     30                        'duration_years',
     31                        'duration_months',
     32                        'duration_days',
     33                        'duration_hours',
     34                        'duration_minutes',
     35                        'duration_seconds',
     36                ];
    3437                if ( ! confirm( PKAPI.clear_message ) ) {
    3538                        return;
    … …  
    3740                $.each( fieldIds, function( count, val ) {
    3841                        document.getElementById( val ).value = '';
    39                 });
     42                } );
    4043                $( '#kind-media-container' ).addClass( 'hidden' );
    4144                $( '#kind-media-container' ).children( 'img' ).hide();
    4245                $( '#add-kind-media' ).show();
    43 
    44 }
    45 
    46 function addhttp( url ) {
    47         if ( ! /^(?:f|ht)tps?\:\/\//.test( url ) ) {
    48                 url = 'http://' + url;
    49         }
    50         return url;
    51 }
    52 
    53 function showLoadingSpinner() {
    54         $( '#replybox-meta' ).addClass( 'is-loading' );
    55 }
    56 
    57 function hideLoadingSpinner() {
    58         $( '#replybox-meta' ).removeClass( 'is-loading' );
    59 }
    60 
    61 //function used to validate website URL
    62 function checkUrl( url ) {
    63 
    64     //regular expression for URL
    65     var pattern = /^(http|https)?:\/\/[a-zA-Z0-9-\.]+\.[a-z]{2,4}/;
    66 
    67     if ( pattern.test( url ) ) {
    68         return true;
    69     } else {
    70         return false;
    71     }
    72 }
    73 
    74 function getLinkPreview() {
    75         if ( '' === $( '#cite_url' ).val() ) {
    76                 return;
    77         }
    78         $.ajax({
    79                 type: 'GET',
    80 
    81                 // Here we supply the endpoint url, as opposed to the action in the data object with the admin-ajax method
    82                 url: PKAPI.api_url + 'parse/',
    83                 beforeSend: function( xhr ) {
    84 
    85                 // Here we set a header 'X-WP-Nonce' with the nonce as opposed to the nonce in the data object with admin-ajax
    86                 xhr.setRequestHeader( 'X-WP-Nonce', PKAPI.api_nonce );
    87                 },
    88                 data: {
    89                         url: $( '#cite_url' ).val(),
    90                         kind: $( 'input[name=\'tax_input[kind]\']:checked' ).val(),
    91                         follow: true
    92                 },
    93                 success: function( response ) {
    94                         var published;
    95                         var updated;
    96                         if ( 'undefined' === typeof response ) {
    97                                 alert( 'Error: Unable to Retrieve' );
     46        }
     47
     48        function showLoadingSpinner() {
     49                $( '#replybox-meta' ).addClass( 'is-loading' );
     50        }
     51
     52        function hideLoadingSpinner() {
     53                $( '#replybox-meta' ).removeClass( 'is-loading' );
     54        }
     55
     56        //function used to validate website URL
     57        function checkUrl( url ) {
     58                //regular expression for URL
     59                var pattern = /^(http|https)?:\/\/[a-zA-Z0-9-\.]+\.[a-z]{2,4}/;
     60
     61                return pattern.test( url );
     62        }
     63
     64        // Fills the Author tab from one author or a list. Each field holds one value
     65        // per author, separated by semicolons, so the values stay paired by position.
     66        function fillAuthors( authors ) {
     67                var fields = { name: [], url: [], photo: [] };
     68                if ( ! Array.isArray( authors ) ) {
     69                        authors = [ authors ];
     70                }
     71                $.each( authors, function( index, author ) {
     72                        if ( 'string' === typeof author ) {
     73                                author = checkUrl( author ) ? { url: author } : { name: author };
     74                        }
     75                        if ( ! author || 'object' !== typeof author ) {
    9876                                return;
    9977                        }
    100                         if ( 'message' in response ) {
    101                                 alert( response.message );
     78                        $.each( fields, function( key, values ) {
     79                                var value = author[ key ];
     80                                if ( Array.isArray( value ) ) {
     81                                        value = value.join( '; ' );
     82                                } else if ( value && 'object' === typeof value ) {
     83                                        value = value.value;
     84                                }
     85                                values.push( 'string' === typeof value ? value : '' );
     86                        } );
     87                } );
     88                $.each( fields, function( key, values ) {
     89                        $( '#cite_author_' + key ).val( values.join( '' ) ? values.join( '; ' ) : '' );
     90                } );
     91        }
     92
     93        function getLinkPreview() {
     94                if ( '' === $( '#cite_url' ).val() ) {
     95                        return;
     96                }
     97                $.ajax( {
     98                        type: 'GET',
     99
     100                        // Here we supply the endpoint url, as opposed to the action in the data object with the admin-ajax method
     101                        url: PKAPI.api_url + 'parse/',
     102                        beforeSend: function( xhr ) {
     103                                // Here we set a header 'X-WP-Nonce' with the nonce as opposed to the nonce in the data object with admin-ajax
     104                                xhr.setRequestHeader( 'X-WP-Nonce', PKAPI.api_nonce );
     105                        },
     106                        data: {
     107                                url: $( '#cite_url' ).val(),
     108                                follow: true,
     109                        },
     110                        success: function( response ) {
     111                                var published, updated, duration;
     112                                if ( 'undefined' === typeof response ) {
     113                                        alert( PKAPI.error_message );
     114                                        return;
     115                                }
     116                                if ( 'message' in response ) {
     117                                        alert( response.message );
     118                                        return;
     119                                }
     120                                if ( 'name' in response ) {
     121                                        $( '#cite_name' ).val( response.name );
     122                                }
     123                                if ( 'published' in response ) {
     124                                        published = moment.parseZone( response.published );
     125                                        $( '#cite_published_date' ).val( published.format( 'YYYY-MM-DD' ) );
     126                                        $( '#cite_published_time' ).val( published.format( 'HH:mm:ss' ) );
     127                                        $( '#cite_published_offset' ).val( published.format( 'Z' ) );
     128                                }
     129                                if ( 'updated' in response ) {
     130                                        updated = moment.parseZone( response.updated );
     131                                        $( '#cite_updated_date' ).val( updated.format( 'YYYY-MM-DD' ) );
     132                                        $( '#cite_updated_time' ).val( updated.format( 'HH:mm:ss' ) );
     133                                        $( '#cite_updated_offset' ).val( updated.format( 'Z' ) );
     134                                }
     135                                if ( 'duration' in response ) {
     136                                        duration = moment.duration( response.duration );
     137                                        $( '#duration_years' ).val( duration.years() );
     138                                        $( '#duration_months' ).val( duration.months() );
     139                                        $( '#duration_days' ).val( duration.days() );
     140                                        $( '#duration_hours' ).val( duration.hours() );
     141                                        $( '#duration_minutes' ).val( duration.minutes() );
     142                                        $( '#duration_seconds' ).val( duration.seconds() );
     143                                }
     144
     145                                if ( 'summary' in response ) {
     146                                        $( '#cite_summary' ).val( response.summary );
     147                                }
     148                                if ( 'featured' in response ) {
     149                                        $( '#cite_featured' ).val( response.featured );
     150                                }
     151                                if ( 'author' in response ) {
     152                                        fillAuthors( response.author );
     153                                }
     154                                if ( 'publication' in response && ( 'string' !== typeof response.publication ) ) {
     155                                        if ( 'name' in response.publication ) {
     156                                                $( '#cite_publication' ).val( response.publication.name );
     157                                        }
     158                                } else {
     159                                        $( '#cite_publication' ).val( response.publication );
     160                                }
     161
     162                                if ( 'category' in response ) {
     163                                        if ( 'object' === typeof response.category ) {
     164                                                $( '#cite_tags' ).val( response.category.join( ';' ) );
     165                                        }
     166                                }
     167                                alert( PKAPI.success_message );
     168                        },
     169                        error: function( jqXHR ) {
     170                                if ( jqXHR.responseJSON && jqXHR.responseJSON.message ) {
     171                                        alert( jqXHR.responseJSON.message );
     172                                } else {
     173                                        alert( PKAPI.error_message );
     174                                }
     175                        },
     176                        complete: hideLoadingSpinner,
     177                } );
     178        }
     179
     180        function changeSettings() {
     181                var kind = $( 'input[name=\'tax_input[kind]\']:checked' ).val();
     182                switch ( kind ) {
     183                        case 'note':
     184                                hideTitle();
     185                                hideReply();
     186                                hideRSVP();
     187                                hideRating();
     188                                hideTime();
     189                                hideMedia();
     190                                break;
     191                        case 'article':
     192                                showTitle();
     193                                hideReply();
     194                                hideRSVP();
     195                                hideRating();
     196                                hideTime();
     197                                hideMedia();
     198                                break;
     199                        case 'issue':
     200                                showTitle();
     201                                showReply();
     202                                hideRSVP();
     203                                hideRating();
     204                                hideTime();
     205                                hideMedia();
     206                                break;
     207                        case 'listen':
     208                        case 'jam':
     209                        case 'watch':
     210                        case 'read':
     211                        case 'play':
     212                                hideTitle();
     213                                showReply();
     214                                hideRSVP();
     215                                showRating();
     216                                showTime();
     217                                hideMedia();
     218                                break;
     219                        case 'photo':
     220                        case 'video':
     221                        case 'audio':
     222                                hideTitle();
     223                                showReply();
     224                                hideRSVP();
     225                                hideRating();
     226                                showTime();
     227                                showMedia();
     228                                break;
     229                        case 'rsvp':
     230                                showReply();
     231                                hideTitle();
     232                                hideTime();
     233                                showRSVP();
     234                                hideRating();
     235                                hideMedia();
     236                                break;
     237                        case 'review':
     238                        case 'drink':
     239                        case 'eat':
     240                                showReply();
     241                                hideTitle();
     242                                hideTime();
     243                                hideRSVP();
     244                                showRating();
     245                                hideMedia();
     246                                break;
     247                        default:
     248                                showReply();
     249                                hideTime();
     250                                hideTitle();
     251                                hideRSVP();
     252                                hideRating();
     253                                hideMedia();
     254                }
     255        }
     256
     257        function showReply() {
     258                $( '#replybox-meta' ).removeClass( 'hidden' );
     259        }
     260
     261        function hideReply() {
     262                $( '#replybox-meta' ).addClass( 'hidden' );
     263        }
     264
     265        function showMedia() {
     266                $( '#add-kind-media' ).removeClass( 'hidden' );
     267        }
     268
     269        function hideMedia() {
     270                $( '#add-kind-media' ).addClass( 'hidden' );
     271        }
     272
     273        function showTitle() {
     274                var titlediv = $( '#titlediv' ).detach();
     275                titlediv.prependTo( '#post-body-content' );
     276        }
     277
     278        function hideTitle() {
     279                var titlediv = $( '#titlediv' ).detach();
     280                titlediv.insertAfter( '#postdivrich' );
     281        }
     282
     283        function showRSVP() {
     284                $( '#rsvp-option' ).removeClass( 'hide-if-js' );
     285        }
     286
     287        function hideRSVP() {
     288                $( '#rsvp-option' ).addClass( 'hide-if-js' );
     289        }
     290
     291        function showRating() {
     292                $( '#rating-option' ).removeClass( 'hide-if-js' );
     293        }
     294
     295        function hideRating() {
     296                $( '#rating-option' ).addClass( 'hide-if-js' );
     297        }
     298
     299        function showTime() {
     300                $( '#kind-time' ).removeClass( 'hide-if-js' );
     301        }
     302
     303        function hideTime() {
     304                $( '#kind-time' ).addClass( 'hide-if-js' );
     305        }
     306
     307        function handleKindMediaWindow() {
     308                'use strict';
     309
     310                var json;
     311
     312                /**
     313                 * If an instance of KindWindow already exists, then we can open it
     314                 * rather than creating a new instance.
     315                 */
     316                if ( undefined !== KindWindow ) {
     317                        KindWindow.open();
     318                        return;
     319                }
     320
     321                KindWindow = wp.media.frames.KindWindow = wp.media( {
     322                        title: PKAPI.media_title,
     323                        button: {
     324                                text: PKAPI.media_button,
     325                        },
     326                        multiple: false,
     327                } );
     328
     329                KindWindow.on( 'select', function() {
     330                        json = KindWindow.state().get( 'selection' ).first().toJSON();
     331                        if ( ! json.url ) {
    102332                                return;
    103333                        }
    104                         if ( 'name' in response ) {
    105                                 $( '#cite_name' ).val( response.name );
    106                         }
    107                         if ( 'published' in response ) {
    108                                 var published = moment.parseZone( response.published );
    109                                 $( '#cite_published_date' ).val( published.format( 'YYYY-MM-DD' ) ) ;
    110                                 $( '#cite_published_time' ).val( published.format( 'HH:mm:ss' ) ) ;
    111                                 $( '#cite_published_offset' ).val( published.format( 'Z' ) );
    112                         }
    113                         if ( 'updated' in response ) {
    114                                 var updated = moment.parseZone( response.updated );
    115                                 $( '#cite_updated_date' ).val( updated.format( 'YYYY-MM-DD' ) ) ;
    116                                 $( '#cite_updated_time' ).val( updated.format( 'HH:mm:ss' ) ) ;
    117                                 $( '#cite_updated_offset' ).val( updated.format( 'Z' ) );
    118                         }
    119                         if ( 'duration' in response ) {
    120                                 var duration = moment.duration( response.duration);
    121                                 $( '#duration_years' ).val( duration.years() );
    122                                 $( '#duration_months' ).val( duration.months() );
    123                                 $( '#duration_days' ).val( duration.days() );
    124                                 $( '#duration_hours' ).val( duration.hours() );
    125                                 $( '#duration_minutes' ).val( duration.minutes() );
    126                                 $( '#duration_seconds' ).val( duration.seconds() );
    127                         }
    128 
    129                         if ( 'summary' in response ) {
    130                                 $( '#cite_summary' ).val( response.summary ) ;
    131                         }
    132                         if ( 'featured' in response ) {
    133                                 $( '#cite_featured' ).val( response.featured ) ;
    134                         }
    135                         if ( ( 'author' in response ) && ( 'string' != typeof response.author ) ) {
    136                                 if ( 'name' in response.author ) {
    137                                         if ( 'string' === typeof response.author.name ) {
    138                                                 $( '#cite_author_name' ).val( response.author.name );
    139                                         } else {
    140                                                 $( '#cite_author_name' ).val( response.author.name.join( ';' ) ) ;
    141                                         }
    142                                 }
    143                                 if ( 'photo' in response.author ) {
    144                                         if ( 'string' === typeof response.author.name ) {
    145                                                 $( '#cite_author_photo' ).val( response.author.photo );
    146                                         } else {
    147                                                 $( '#cite_author_photo' ).val( response.author.photo.join( ';' ) ) ;
    148                                         }
    149                                 }
    150                                 if ( 'url' in response.author ) {
    151                                         if ( 'string' === typeof response.author.url ) {
    152                                                 $( '#cite_author_url' ).val( response.author.url );
    153                                         } else {
    154                                                 $( '#cite_author_url' ).val( response.author.url.join( ';' ) ) ;
    155                                         }
    156                                 }
    157                         }
    158                         if ( 'publication' in response && ( 'string' != typeof response.publication ) ) {
    159                                 if ( 'name' in response.publication ) {
    160                                         $( '#cite_publication' ).val( response.publication.name );
    161                                 }
     334                        $( '#cite_name' ).val( json.title );
     335                        $( '#cite_url' ).val( json.url );
     336                        $( '#cite_media' ).val( json.id );
     337                        $( '#cite_summary' ).val( json.description );
     338                        $( '#kind-media-container' )
     339                                .children( 'img' )
     340                                .attr( 'src', json.url )
     341                                .attr( 'alt', json.caption )
     342                                .attr( 'title', json.title )
     343                                .show()
     344                                .parent()
     345                                .removeClass( 'hidden' );
     346
     347                        $( '#add-kind-media' ).hide();
     348                } );
     349                KindWindow.open();
     350        }
     351
     352        jQuery( document )
     353                .on( 'change', '#taxonomy-kind', function( event ) {
     354                        changeSettings();
     355                        event.preventDefault();
     356                } )
     357                .on( 'blur', '#cite_url', function( event ) {
     358                        if ( '' !== $( '#cite_url' ).val() ) {
     359                                if ( false === checkUrl( $( '#cite_url' ).val() ) ) {
     360                                        alert( PKAPI.invalid_url );
     361                                } else if ( '' === $( '#cite_name' ).val() ) {
     362                                        showLoadingSpinner();
     363                                        getLinkPreview();
     364                                }
     365                                event.preventDefault();
     366                        }
     367                } )
     368                .on( 'click', '.clear-kindmeta-button', function( event ) {
     369                        clearPostProperties();
     370                        event.preventDefault();
     371                } )
     372                .on( 'click', 'a.show-kind-details', function( event ) {
     373                        if ( $( '#kind-details' ).is( ':hidden' ) ) {
     374                                $( '#kind-author' ).slideUp( 'fast' ).siblings( 'a.show-kind-author' );
     375                                $( '#kind-details' ).slideDown( 'fast' ).siblings( 'a.hide-kind-details' ).show().focus();
    162376                        } else {
    163                                 $( '#cite_publication' ).val( response.publication ) ;
    164                         }
    165 
    166                         if ( 'category' in response ) {
    167                                 if ( 'object' === typeof response.category ) {
    168                                         $( '#cite_tags' ).val( response.category.join( ';' ) );
    169                                 }
    170                         }
    171                 alert( PKAPI.success_message );
    172                 console.log( response );
    173                 },
    174                 fail: function( response ) {
    175                         console.log( response );
    176                         alert( response.message );
    177                 },
    178                 error: function( jqXHR, textStatus, errorThrown ) {
    179                         alert( jqXHR.responseJSON.message );
    180                         console.log( jqXHR );
    181                 },
    182                 always: hideLoadingSpinner()
    183         });
    184 }
    185 
    186 function changeSettings() {
    187         kind = $( 'input[name=\'tax_input[kind]\']:checked' ).val();
    188         switch ( kind ) {
    189                 case 'note':
    190                         hideTitle();
    191                         hideReply();
    192                         hideRSVP();
    193                         hideRating();
    194                         hideTime();
    195                         hideMedia();
    196                         break;
    197                 case 'article':
    198                         showTitle();
    199                         hideReply();
    200                         hideRSVP();
    201                         hideRating();
    202                         hideTime();
    203                         hideMedia();
    204                         break;
    205                 case 'issue':
    206                         showTitle();
    207                         showReply();
    208                         hideRSVP();
    209                         hideRating();
    210                         hideTime();
    211                         hideMedia();
    212                         break;
    213                 case 'listen':
    214                 case 'jam':
    215                 case 'watch':
    216                 case 'read':
    217                 case 'play':
    218                         hideTitle();
    219                         showReply();
    220                         hideRSVP();
    221                         showRating();
    222                         showTime();
    223                         hideMedia();
    224                         break;
    225                 case 'photo':
    226                 case 'video':
    227                 case 'audio':
    228                         hideTitle();
    229                         showReply();
    230                         hideRSVP();
    231                         hideRating();
    232                         showTime();
    233                         showMedia();
    234                         break;
    235                 case 'rsvp':
    236                         showReply();
    237                         hideTitle();
    238                         hideTime();
    239                         showRSVP();
    240                         hideRating();
    241                         hideMedia();
    242                         break;
    243                 case 'review':
    244                 case 'drink':
    245                 case 'eat':
    246                         showReply();
    247                         hideTitle();
    248                         hideTime();
    249                         hideRSVP();
    250                         showRating();
    251                         hideMedia();
    252                         break;
    253                 default:
    254                         showReply();
    255                         hideTime();
    256                         hideTitle();
    257                         hideRSVP();
    258                         hideRating();
    259                         hideMedia();
    260         }
    261 }
    262 
    263 function showReply() {
    264         $( '#replybox-meta' ).removeClass( 'hidden' );
    265 }
    266 
    267 function hideReply() {
    268         $( '#replybox-meta' ).addClass( 'hidden' );
    269 }
    270 
    271 function showMedia() {
    272         $( '#add-kind-media' ).removeClass( 'hidden' );
    273 }
    274 
    275 function hideMedia() {
    276         $( '#add-kind-media' ).addClass( 'hidden' );
    277 }
    278 
    279 
    280 function showTitle() {
    281         var titlediv = $( '#titlediv' ).detach();
    282         titlediv.prependTo( '#post-body-content' );
    283 }
    284 
    285 function hideTitle() {
    286         var titlediv = $( '#titlediv' ).detach();
    287         titlediv.insertAfter( '#postdivrich' );
    288 }
    289 
    290 function showRSVP() {
    291         $( '#rsvp-option' ).removeClass( 'hide-if-js' );
    292 }
    293 
    294 function hideRSVP() {
    295         $( '#rsvp-option' ).addClass( 'hide-if-js' );
    296 }
    297 
    298 function showRating() {
    299         $( '#rating-option' ).removeClass( 'hide-if-js' );
    300 }
    301 
    302 function hideRating() {
    303         $( '#rating-option' ).addClass( 'hide-if-js' );
    304 }
    305 
    306 function showTime() {
    307         $( '#kind-time' ).removeClass( 'hide-if-js' );
    308 }
    309 
    310 function hideTime() {
    311         $( '#kind-time' ).addClass( 'hide-if-js' );
    312 }
    313 
    314 function handleKindMediaWindow() {
    315         'use strict';
    316 
    317         var KindWindow, ImageData, json;
    318 
    319         /**
    320          * If an instance of KindWindow already exists, then we can open it
    321          * rather than creating a new instance.
    322          */
    323         if ( undefined !== KindWindow ) {
    324                 KindWindow.open();
    325                 return;
    326         }
    327 
    328         KindWindow = wp.media.frames.KindWindow = wp.media({
    329                 title: 'Attach',
    330                 button: {
    331                         text: 'Use this media'
    332                 },
    333                 multiple: false
    334         });
    335 
    336         KindWindow.on( 'select', function() {
    337                 json = KindWindow.state().get( 'selection' ).first().toJSON();
    338                 console.log( json );
    339                 if ( 0 > $.trim( json.url.length ) ) {
    340                         return;
    341                 }
    342                 $( '#cite_name' ).val( json.title );
    343                 $( '#cite_url' ).val( json.url );
    344                 $( '#cite_media' ).val( json.id );
    345                 $( '#cite_summary' ).val( json.description );
    346                 $( '#kind-media-container' )
    347                         .children( 'img' )
    348                         .attr( 'src', json.url )
    349                         .attr( 'alt', json.caption )
    350                         .attr( 'title', json.title )
    351                         .show()
    352                         .parent()
    353                         .removeClass( 'hidden' );
    354 
    355                 $( '#add-kind-media' ).hide();
    356         });
    357         KindWindow.open();
    358 
    359 }
    360 
    361 jQuery( document )
    362         .on( 'change', '#taxonomy-kind', function( event ) {
    363                 changeSettings();
    364                 event.preventDefault();
    365         })
    366         .on( 'blur', '#cite_url', function( event ) {
    367                 if ( '' !== $( '#cite_url' ).val() ) {
    368                         if ( false == checkUrl( $( '#cite_url' ).val() ) ) {
    369                                 alert( 'Invalid URL' );
    370                         } else if ( '' === $( '#cite_name' ).val() ) {
    371                                 showLoadingSpinner();
    372                                 getLinkPreview();
    373                         }
    374                         event.preventDefault();
    375                 }
    376         })
    377         .on( 'click', '.clear-kindmeta-button', function( event ) {
    378                 clearPostProperties();
    379                 event.preventDefault();
    380         })
    381         .on( 'click', 'a.show-kind-details', function( event ) {
    382                 if ( $( '#kind-details' ).is( ':hidden' ) ) {
    383                         $( '#kind-author' ).slideUp( 'fast' ).siblings( 'a.show-kind-author' );
    384                         $( '#kind-details' ).slideDown( 'fast' ).siblings( 'a.hide-kind-details' ).show().focus();
    385                 } else {
    386                         $( '#kind-details' ).slideUp( 'fast' ).siblings( 'a.show-kind-details' ).focus();
    387                 }
    388                 event.preventDefault();
    389         })
    390         .on( 'click', 'a.show-kind-author-details', function( event ) {
    391                 if ( $( '#kind-author' ).is( ':hidden' ) ) {
    392                         $( '#kind-details' ).slideUp( 'fast' ).siblings( 'a.show-kind-details' );
    393                         $( '#kind-author' ).slideDown( 'fast' ).siblings( 'a.hide-kind-author' ).show().focus();
    394                 } else {
    395                         $( '#kind-author' ).slideUp( 'fast' ).siblings( 'a.show-kind-author' ).focus();
    396                 }
    397                 event.preventDefault();
    398         })
    399         .on( 'click', '#add-kind-media', function( event ) {
    400                 event.preventDefault();
    401                 handleKindMediaWindow();
    402         });
    403 });
     377                                $( '#kind-details' ).slideUp( 'fast' ).siblings( 'a.show-kind-details' ).focus();
     378                        }
     379                        event.preventDefault();
     380                } )
     381                .on( 'click', 'a.show-kind-author-details', function( event ) {
     382                        if ( $( '#kind-author' ).is( ':hidden' ) ) {
     383                                $( '#kind-details' ).slideUp( 'fast' ).siblings( 'a.show-kind-details' );
     384                                $( '#kind-author' ).slideDown( 'fast' ).siblings( 'a.hide-kind-author' ).show().focus();
     385                        } else {
     386                                $( '#kind-author' ).slideUp( 'fast' ).siblings( 'a.show-kind-author' ).focus();
     387                        }
     388                        event.preventDefault();
     389                } )
     390                .on( 'click', '#add-kind-media', function( event ) {
     391                        event.preventDefault();
     392                        handleKindMediaWindow();
     393                } );
     394} );
  • indieweb-post-kinds/trunk/languages/indieweb-post-kinds.pot

    r3067251 r3738687  
    1 # Copyright (C) 2024 David Shanske
    2 # This file is distributed under the same license as the Post Kinds plugin.
     1# Copyright (C) 2026 David Shanske
     2# This file is distributed under the GPLv2 or later.
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Post Kinds 3.7.3\n"
     5"Project-Id-Version: Post Kinds 4.0.0\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/indieweb-post-kinds\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2024-04-09T03:10:32+00:00\n"
     12"POT-Creation-Date: 2026-10-10T20:26:16+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    14 "X-Generator: WP-CLI 2.10.0\n"
     14"X-Generator: WP-CLI 2.12.0\n"
    1515"X-Domain: indieweb-post-kinds\n"
    1616
    1717#. Plugin Name of the plugin
    18 #: indieweb-post-kinds.php
    19 #: includes/class-kind-config.php:251
    20 #: includes/class-kind-config.php:252
    21 #: includes/class-kind-config.php:258
     18#: indieweb-post-kinds.php:7
     19#: includes/class-kind-config.php:433
     20#: includes/class-kind-config.php:434
     21#: includes/class-kind-config.php:440
    2222msgid "Post Kinds"
    2323msgstr ""
    2424
    2525#. Plugin URI of the plugin
    26 #: indieweb-post-kinds.php
     26#: indieweb-post-kinds.php:8
    2727msgid "https://wordpress.org/plugins/indieweb-post-kinds/"
    2828msgstr ""
    2929
    3030#. Description of the plugin
    31 #: indieweb-post-kinds.php
     31#: indieweb-post-kinds.php:9
    3232msgid "Ever want to reply to someone else's post with a post on your own site? Or to \"like\" someone else's post, but with your own site?"
    3333msgstr ""
    3434
    3535#. Author of the plugin
    36 #: indieweb-post-kinds.php
     36#: indieweb-post-kinds.php:15
    3737msgid "David Shanske"
    3838msgstr ""
    3939
    4040#. Author URI of the plugin
    41 #: indieweb-post-kinds.php
     41#: indieweb-post-kinds.php:16
    4242msgid "https://david.shanske.com"
    4343msgstr ""
    4444
    45 #: includes/class-kind-config.php:96
     45#: includes/class-kind-config.php:224
     46msgid "The allowed HTML must be a valid JSON object. The previous value has been kept."
     47msgstr ""
     48
     49#: includes/class-kind-config.php:261
     50msgid "Kind base"
     51msgstr ""
     52
     53#: includes/class-kind-config.php:269
    4654msgid "Content Options"
    4755msgstr ""
    4856
    49 #: includes/class-kind-config.php:105
     57#: includes/class-kind-config.php:278
    5058msgid "Select All Kinds You Wish to Use"
    5159msgstr ""
    5260
    53 #: includes/class-kind-config.php:112
     61#: includes/class-kind-config.php:285
    5462msgid "Default Kind for New Posts"
    5563msgstr ""
    5664
    57 #: includes/class-kind-config.php:120
     65#: includes/class-kind-config.php:293
    5866msgid "Embed Sites into your Response"
    5967msgstr ""
    6068
    61 #: includes/class-kind-config.php:131
     69#: includes/class-kind-config.php:304
    6270msgid "Kinds to Show in Main Feed. Selecting None will Disable this Filter"
    6371msgstr ""
    6472
    65 #: includes/class-kind-config.php:142
     73#: includes/class-kind-config.php:315
    6674msgid "Move Response to Bottom of Content"
    6775msgstr ""
    6876
    69 #: includes/class-kind-config.php:153
     77#: includes/class-kind-config.php:326
    7078msgid "Display Before Kind"
    7179msgstr ""
    7280
    73 #: includes/class-kind-config.php:161
     81#: includes/class-kind-config.php:334
    7482msgid "Show Icon"
    7583msgstr ""
    7684
    77 #: includes/class-kind-config.php:162
     85#: includes/class-kind-config.php:335
    7886msgid "Show Text"
    7987msgstr ""
    8088
    81 #: includes/class-kind-config.php:163
     89#: includes/class-kind-config.php:336
    8290msgid "Show Icon and Text"
    8391msgstr ""
    8492
    85 #: includes/class-kind-config.php:164
     93#: includes/class-kind-config.php:337
    8694msgid "Display Nothing"
    8795msgstr ""
    8896
    89 #: includes/class-kind-config.php:172
     97#: includes/class-kind-config.php:345
    9098msgid "Response Content Allowed Html Elements"
    9199msgstr ""
    92100
    93 #: includes/class-kind-config.php:183
     101#: includes/class-kind-config.php:356
    94102msgid "Automatically add the Kind to the Title"
    95103msgstr ""
    96104
    97 #: includes/class-kind-config.php:391
     105#: includes/class-kind-config.php:593
    98106msgid "Indieweb Post Kinds"
    99107msgstr ""
    100108
    101 #: includes/class-kind-config.php:392
     109#: includes/class-kind-config.php:594
    102110msgid "Adds support for responding and interacting with other sites."
    103111msgstr ""
    104112
    105 #: includes/class-kind-config.php:415
     113#: includes/class-kind-config.php:619
    106114msgid "Post Properties"
    107115msgstr ""
    108116
    109 #: includes/class-kind-config.php:416
     117#: includes/class-kind-config.php:620
    110118msgid ""
    111119"\n"
    … …  
    139147msgstr ""
    140148
    141 #: includes/class-kind-fields.php:27
     149#: includes/class-kind-fields.php:39
    142150msgid "(GMT-11:00) Niue"
    143151msgstr ""
    144152
    145 #: includes/class-kind-fields.php:28
     153#: includes/class-kind-fields.php:40
    146154msgid "(GMT-11:00) Pago Pago"
    147155msgstr ""
    148156
    149 #: includes/class-kind-fields.php:29
     157#: includes/class-kind-fields.php:41
    150158msgid "(GMT-10:00) Hawaii Time"
    151159msgstr ""
    152160
    153 #: includes/class-kind-fields.php:30
     161#: includes/class-kind-fields.php:42
    154162msgid "(GMT-10:00) Rarotonga"
    155163msgstr ""
    156164
    157 #: includes/class-kind-fields.php:31
     165#: includes/class-kind-fields.php:43
    158166msgid "(GMT-10:00) Tahiti"
    159167msgstr ""
    160168
    161 #: includes/class-kind-fields.php:32
     169#: includes/class-kind-fields.php:44
    162170msgid "(GMT-09:30) Marquesas"
    163171msgstr ""
    164172
    165 #: includes/class-kind-fields.php:33
     173#: includes/class-kind-fields.php:45
    166174msgid "(GMT-09:00) Alaska Time"
    167175msgstr ""
    168176
    169 #: includes/class-kind-fields.php:34
     177#: includes/class-kind-fields.php:46
    170178msgid "(GMT-09:00) Gambier"
    171179msgstr ""
    172180
    173 #: includes/class-kind-fields.php:35
     181#: includes/class-kind-fields.php:47
    174182msgid "(GMT-08:00) Pacific Time"
    175183msgstr ""
    176184
    177 #: includes/class-kind-fields.php:36
     185#: includes/class-kind-fields.php:48
    178186msgid "(GMT-08:00) Pacific Time - Tijuana"
    179187msgstr ""
    180188
    181 #: includes/class-kind-fields.php:37
     189#: includes/class-kind-fields.php:49
    182190msgid "(GMT-08:00) Pacific Time - Vancouver"
    183191msgstr ""
    184192
    185 #: includes/class-kind-fields.php:38
     193#: includes/class-kind-fields.php:50
    186194msgid "(GMT-08:00) Pacific Time - Whitehorse"
    187195msgstr ""
    188196
    189 #: includes/class-kind-fields.php:39
     197#: includes/class-kind-fields.php:51
    190198msgid "(GMT-08:00) Pitcairn"
    191199msgstr ""
    192200
    193 #: includes/class-kind-fields.php:40
     201#: includes/class-kind-fields.php:52
    194202msgid "(GMT-07:00) Mountain Time - Dawson Creek"
    195203msgstr ""
    196204
    197 #: includes/class-kind-fields.php:41
     205#: includes/class-kind-fields.php:53
    198206msgid "(GMT-07:00) Mountain Time"
    199207msgstr ""
    200208
    201 #: includes/class-kind-fields.php:42
     209#: includes/class-kind-fields.php:54
    202210msgid "(GMT-07:00) Mountain Time - Edmonton"
    203211msgstr ""
    204212
    205 #: includes/class-kind-fields.php:43
     213#: includes/class-kind-fields.php:55
    206214msgid "(GMT-07:00) Mountain Time - Hermosillo"
    207215msgstr ""
    208216
    209 #: includes/class-kind-fields.php:44
     217#: includes/class-kind-fields.php:56
    210218msgid "(GMT-07:00) Mountain Time - Chihuahua, Mazatlan"
    211219msgstr ""
    212220
    213 #: includes/class-kind-fields.php:45
     221#: includes/class-kind-fields.php:57
    214222msgid "(GMT-07:00) Mountain Time - Arizona"
    215223msgstr ""
    216224
    217 #: includes/class-kind-fields.php:46
     225#: includes/class-kind-fields.php:58
    218226msgid "(GMT-07:00) Mountain Time - Yellowknife"
    219227msgstr ""
    220228
    221 #: includes/class-kind-fields.php:47
     229#: includes/class-kind-fields.php:59
    222230msgid "(GMT-06:00) Belize"
    223231msgstr ""
    224232
    225 #: includes/class-kind-fields.php:48
     233#: includes/class-kind-fields.php:60
    226234msgid "(GMT-06:00) Central Time"
    227235msgstr ""
    228236
    229 #: includes/class-kind-fields.php:49
     237#: includes/class-kind-fields.php:61
    230238msgid "(GMT-06:00) Costa Rica"
    231239msgstr ""
    232240
    233 #: includes/class-kind-fields.php:50
     241#: includes/class-kind-fields.php:62
    234242msgid "(GMT-06:00) El Salvador"
    235243msgstr ""
    236244
    237 #: includes/class-kind-fields.php:51
     245#: includes/class-kind-fields.php:63
    238246msgid "(GMT-06:00) Guatemala"
    239247msgstr ""
    240248
    241 #: includes/class-kind-fields.php:52
     249#: includes/class-kind-fields.php:64
    242250msgid "(GMT-06:00) Managua"
    243251msgstr ""
    244252
    245 #: includes/class-kind-fields.php:53
     253#: includes/class-kind-fields.php:65
    246254msgid "(GMT-06:00) Central Time - Mexico City"
    247255msgstr ""
    248256
    249 #: includes/class-kind-fields.php:54
     257#: includes/class-kind-fields.php:66
    250258msgid "(GMT-06:00) Central Time - Regina"
    251259msgstr ""
    252260
    253 #: includes/class-kind-fields.php:55
     261#: includes/class-kind-fields.php:67
    254262msgid "(GMT-06:00) Central Time - Tegucigalpa"
    255263msgstr ""
    256264
    257 #: includes/class-kind-fields.php:56
     265#: includes/class-kind-fields.php:68
    258266msgid "(GMT-06:00) Central Time - Winnipeg"
    259267msgstr ""
    260268
    261 #: includes/class-kind-fields.php:57
     269#: includes/class-kind-fields.php:69
    262270msgid "(GMT-06:00) Galapagos"
    263271msgstr ""
    264272
    265 #: includes/class-kind-fields.php:58
     273#: includes/class-kind-fields.php:70
    266274msgid "(GMT-05:00) Bogota"
    267275msgstr ""
    268276
    269 #: includes/class-kind-fields.php:59
     277#: includes/class-kind-fields.php:71
    270278msgid "(GMT-05:00) America Cancun"
    271279msgstr ""
    272280
    273 #: includes/class-kind-fields.php:60
     281#: includes/class-kind-fields.php:72
    274282msgid "(GMT-05:00) Cayman"
    275283msgstr ""
    276284
    277 #: includes/class-kind-fields.php:61
     285#: includes/class-kind-fields.php:73
    278286msgid "(GMT-05:00) Guayaquil"
    279287msgstr ""
    280288
    281 #: includes/class-kind-fields.php:62
     289#: includes/class-kind-fields.php:74
    282290msgid "(GMT-05:00) Havana"
    283291msgstr ""
    284292
    285 #: includes/class-kind-fields.php:63
     293#: includes/class-kind-fields.php:75
    286294msgid "(GMT-05:00) Eastern Time - Iqaluit"
    287295msgstr ""
    288296
    289 #: includes/class-kind-fields.php:64
     297#: includes/class-kind-fields.php:76
    290298msgid "(GMT-05:00) Jamaica"
    291299msgstr ""
    292300
    293 #: includes/class-kind-fields.php:65
     301#: includes/class-kind-fields.php:77
    294302msgid "(GMT-05:00) Lima"
    295303msgstr ""
    296304
    297 #: includes/class-kind-fields.php:66
     305#: includes/class-kind-fields.php:78
    298306msgid "(GMT-05:00) Nassau"
    299307msgstr ""
    300308
    301 #: includes/class-kind-fields.php:67
     309#: includes/class-kind-fields.php:79
    302310msgid "(GMT-05:00) Eastern Time"
    303311msgstr ""
    304312
    305 #: includes/class-kind-fields.php:68
     313#: includes/class-kind-fields.php:80
    306314msgid "(GMT-05:00) Panama"
    307315msgstr ""
    308316
    309 #: includes/class-kind-fields.php:69
     317#: includes/class-kind-fields.php:81
    310318msgid "(GMT-05:00) Port-au-Prince"
    311319msgstr ""
    312320
    313 #: includes/class-kind-fields.php:70
     321#: includes/class-kind-fields.php:82
    314322msgid "(GMT-05:00) Rio Branco"
    315323msgstr ""
    316324
    317 #: includes/class-kind-fields.php:71
     325#: includes/class-kind-fields.php:83
    318326msgid "(GMT-05:00) Eastern Time - Toronto"
    319327msgstr ""
    320328
    321 #: includes/class-kind-fields.php:72
     329#: includes/class-kind-fields.php:84
    322330msgid "(GMT-05:00) Easter Island"
    323331msgstr ""
    324332
    325 #: includes/class-kind-fields.php:73
     333#: includes/class-kind-fields.php:85
    326334msgid "(GMT-04:30) Caracas"
    327335msgstr ""
    328336
    329 #: includes/class-kind-fields.php:74
     337#: includes/class-kind-fields.php:86
    330338msgid "(GMT-03:00) Asuncion"
    331339msgstr ""
    332340
    333 #: includes/class-kind-fields.php:75
     341#: includes/class-kind-fields.php:87
    334342msgid "(GMT-04:00) Barbados"
    335343msgstr ""
    336344
    337 #: includes/class-kind-fields.php:76
     345#: includes/class-kind-fields.php:88
    338346msgid "(GMT-04:00) Boa Vista"
    339347msgstr ""
    340348
    341 #: includes/class-kind-fields.php:77
     349#: includes/class-kind-fields.php:89
    342350msgid "(GMT-03:00) Campo Grande"
    343351msgstr ""
    344352
    345 #: includes/class-kind-fields.php:78
     353#: includes/class-kind-fields.php:90
    346354msgid "(GMT-03:00) Cuiaba"
    347355msgstr ""
    348356
    349 #: includes/class-kind-fields.php:79
     357#: includes/class-kind-fields.php:91
    350358msgid "(GMT-04:00) Curacao"
    351359msgstr ""
    352360
    353 #: includes/class-kind-fields.php:80
     361#: includes/class-kind-fields.php:92
    354362msgid "(GMT-04:00) Grand Turk"
    355363msgstr ""
    356364
    357 #: includes/class-kind-fields.php:81
     365#: includes/class-kind-fields.php:93
    358366msgid "(GMT-04:00) Guyana"
    359367msgstr ""
    360368
    361 #: includes/class-kind-fields.php:82
     369#: includes/class-kind-fields.php:94
    362370msgid "(GMT-04:00) Atlantic Time - Halifax"
    363371msgstr ""
    364372
    365 #: includes/class-kind-fields.php:83
     373#: includes/class-kind-fields.php:95
    366374msgid "(GMT-04:00) La Paz"
    367375msgstr ""
    368376
    369 #: includes/class-kind-fields.php:84
     377#: includes/class-kind-fields.php:96
    370378msgid "(GMT-04:00) Manaus"
    371379msgstr ""
    372380
    373 #: includes/class-kind-fields.php:85
     381#: includes/class-kind-fields.php:97
    374382msgid "(GMT-04:00) Martinique"
    375383msgstr ""
    376384
    377 #: includes/class-kind-fields.php:86
     385#: includes/class-kind-fields.php:98
    378386msgid "(GMT-04:00) Port of Spain"
    379387msgstr ""
    380388
    381 #: includes/class-kind-fields.php:87
     389#: includes/class-kind-fields.php:99
    382390msgid "(GMT-04:00) Porto Velho"
    383391msgstr ""
    384392
    385 #: includes/class-kind-fields.php:88
     393#: includes/class-kind-fields.php:100
    386394msgid "(GMT-04:00) Puerto Rico"
    387395msgstr ""
    388396
    389 #: includes/class-kind-fields.php:89
     397#: includes/class-kind-fields.php:101
    390398msgid "(GMT-04:00) Santo Domingo"
    391399msgstr ""
    392400
    393 #: includes/class-kind-fields.php:90
     401#: includes/class-kind-fields.php:102
    394402msgid "(GMT-04:00) Thule"
    395403msgstr ""
    396404
    397 #: includes/class-kind-fields.php:91
     405#: includes/class-kind-fields.php:103
    398406msgid "(GMT-04:00) Bermuda"
    399407msgstr ""
    400408
    401 #: includes/class-kind-fields.php:92
     409#: includes/class-kind-fields.php:104
    402410msgid "(GMT-03:30) Newfoundland Time - St. Johns"
    403411msgstr ""
    404412
    405 #: includes/class-kind-fields.php:93
     413#: includes/class-kind-fields.php:105
    406414msgid "(GMT-03:00) Araguaina"
    407415msgstr ""
    408416
    409 #: includes/class-kind-fields.php:94
     417#: includes/class-kind-fields.php:106
    410418msgid "(GMT-03:00) Buenos Aires"
    411419msgstr ""
    412420
    413 #: includes/class-kind-fields.php:95
     421#: includes/class-kind-fields.php:107
    414422msgid "(GMT-03:00) Salvador"
    415423msgstr ""
    416424
    417 #: includes/class-kind-fields.php:96
     425#: includes/class-kind-fields.php:108
    418426msgid "(GMT-03:00) Belem"
    419427msgstr ""
    420428
    421 #: includes/class-kind-fields.php:97
     429#: includes/class-kind-fields.php:109
    422430msgid "(GMT-03:00) Cayenne"
    423431msgstr ""
    424432
    425 #: includes/class-kind-fields.php:98
     433#: includes/class-kind-fields.php:110
    426434msgid "(GMT-03:00) Fortaleza"
    427435msgstr ""
    428436
    429 #: includes/class-kind-fields.php:99
     437#: includes/class-kind-fields.php:111
    430438msgid "(GMT-03:00) Godthab"
    431439msgstr ""
    432440
    433 #: includes/class-kind-fields.php:100
     441#: includes/class-kind-fields.php:112
    434442msgid "(GMT-03:00) Maceio"
    435443msgstr ""
    436444
    437 #: includes/class-kind-fields.php:101
     445#: includes/class-kind-fields.php:113
    438446msgid "(GMT-03:00) Miquelon"
    439447msgstr ""
    440448
    441 #: includes/class-kind-fields.php:102
     449#: includes/class-kind-fields.php:114
    442450msgid "(GMT-03:00) Montevideo"
    443451msgstr ""
    444452
    445 #: includes/class-kind-fields.php:103
     453#: includes/class-kind-fields.php:115
    446454msgid "(GMT-03:00) Paramaribo"
    447455msgstr ""
    448456
    449 #: includes/class-kind-fields.php:104
     457#: includes/class-kind-fields.php:116
    450458msgid "(GMT-03:00) Recife"
    451459msgstr ""
    452460
    453 #: includes/class-kind-fields.php:105
     461#: includes/class-kind-fields.php:117
    454462msgid "(GMT-03:00) Santiago"
    455463msgstr ""
    456464
    457 #: includes/class-kind-fields.php:106
     465#: includes/class-kind-fields.php:118
    458466msgid "(GMT-02:00) Sao Paulo"
    459467msgstr ""
    460468
    461 #: includes/class-kind-fields.php:107
     469#: includes/class-kind-fields.php:119
    462470msgid "(GMT-03:00) Palmer"
    463471msgstr ""
    464472
    465 #: includes/class-kind-fields.php:108
     473#: includes/class-kind-fields.php:120
    466474msgid "(GMT-03:00) Rothera"
    467475msgstr ""
    468476
    469 #: includes/class-kind-fields.php:109
     477#: includes/class-kind-fields.php:121
    470478msgid "(GMT-03:00) Stanley"
    471479msgstr ""
    472480
    473 #: includes/class-kind-fields.php:110
     481#: includes/class-kind-fields.php:122
    474482msgid "(GMT-02:00) Noronha"
    475483msgstr ""
    476484
    477 #: includes/class-kind-fields.php:111
     485#: includes/class-kind-fields.php:123
    478486msgid "(GMT-02:00) South Georgia"
    479487msgstr ""
    480488
    481 #: includes/class-kind-fields.php:112
     489#: includes/class-kind-fields.php:124
    482490msgid "(GMT-01:00) Scoresbysund"
    483491msgstr ""
    484492
    485 #: includes/class-kind-fields.php:113
     493#: includes/class-kind-fields.php:125
    486494msgid "(GMT-01:00) Azores"
    487495msgstr ""
    488496
    489 #: includes/class-kind-fields.php:114
     497#: includes/class-kind-fields.php:126
    490498msgid "(GMT-01:00) Cape Verde"
    491499msgstr ""
    492500
    493 #: includes/class-kind-fields.php:115
     501#: includes/class-kind-fields.php:127
    494502msgid "(GMT+00:00) Abidjan"
    495503msgstr ""
    496504
    497 #: includes/class-kind-fields.php:116
     505#: includes/class-kind-fields.php:128
    498506msgid "(GMT+00:00) Accra"
    499507msgstr ""
    500508
    501 #: includes/class-kind-fields.php:117
     509#: includes/class-kind-fields.php:129
    502510msgid "(GMT+00:00) Bissau"
    503511msgstr ""
    504512
    505 #: includes/class-kind-fields.php:118
     513#: includes/class-kind-fields.php:130
    506514msgid "(GMT+00:00) Casablanca"
    507515msgstr ""
    508516
    509 #: includes/class-kind-fields.php:119
     517#: includes/class-kind-fields.php:131
    510518msgid "(GMT+00:00) El Aaiun"
    511519msgstr ""
    512520
    513 #: includes/class-kind-fields.php:120
     521#: includes/class-kind-fields.php:132
    514522msgid "(GMT+00:00) Monrovia"
    515523msgstr ""
    516524
    517 #: includes/class-kind-fields.php:121
     525#: includes/class-kind-fields.php:133
    518526msgid "(GMT+00:00) Danmarkshavn"
    519527msgstr ""
    520528
    521 #: includes/class-kind-fields.php:122
     529#: includes/class-kind-fields.php:134
    522530msgid "(GMT+00:00) Canary Islands"
    523531msgstr ""
    524532
    525 #: includes/class-kind-fields.php:123
     533#: includes/class-kind-fields.php:135
    526534msgid "(GMT+00:00) Faeroe"
    527535msgstr ""
    528536
    529 #: includes/class-kind-fields.php:124
     537#: includes/class-kind-fields.php:136
    530538msgid "(GMT+00:00) Reykjavik"
    531539msgstr ""
    532540
    533 #: includes/class-kind-fields.php:125
     541#: includes/class-kind-fields.php:137
    534542msgid "(GMT+00:00) GMT (no daylight saving)"
    535543msgstr ""
    536544
    537 #: includes/class-kind-fields.php:126
     545#: includes/class-kind-fields.php:138
    538546msgid "(GMT+00:00) Dublin"
    539547msgstr ""
    540548
    541 #: includes/class-kind-fields.php:127
     549#: includes/class-kind-fields.php:139
    542550msgid "(GMT+00:00) Lisbon"
    543551msgstr ""
    544552
    545 #: includes/class-kind-fields.php:128
     553#: includes/class-kind-fields.php:140
    546554msgid "(GMT+00:00) London"
    547555msgstr ""
    548556
    549 #: includes/class-kind-fields.php:129
     557#: includes/class-kind-fields.php:141
    550558msgid "(GMT+01:00) Algiers"
    551559msgstr ""
    552560
    553 #: includes/class-kind-fields.php:130
     561#: includes/class-kind-fields.php:142
    554562msgid "(GMT+01:00) Ceuta"
    555563msgstr ""
    556564
    557 #: includes/class-kind-fields.php:131
     565#: includes/class-kind-fields.php:143
    558566msgid "(GMT+01:00) Lagos"
    559567msgstr ""
    560568
    561 #: includes/class-kind-fields.php:132
     569#: includes/class-kind-fields.php:144
    562570msgid "(GMT+01:00) Ndjamena"
    563571msgstr ""
    564572
    565 #: includes/class-kind-fields.php:133
     573#: includes/class-kind-fields.php:145
    566574msgid "(GMT+01:00) Tunis"
    567575msgstr ""
    568576
    569 #: includes/class-kind-fields.php:134
     577#: includes/class-kind-fields.php:146
    570578msgid "(GMT+02:00) Windhoek"
    571579msgstr ""
    572580
    573 #: includes/class-kind-fields.php:135
     581#: includes/class-kind-fields.php:147
    574582msgid "(GMT+01:00) Amsterdam"
    575583msgstr ""
    576584
    577 #: includes/class-kind-fields.php:136
     585#: includes/class-kind-fields.php:148
    578586msgid "(GMT+01:00) Andorra"
    579587msgstr ""
    580588
    581 #: includes/class-kind-fields.php:137
     589#: includes/class-kind-fields.php:149
    582590msgid "(GMT+01:00) Central European Time - Belgrade"
    583591msgstr ""
    584592
    585 #: includes/class-kind-fields.php:138
     593#: includes/class-kind-fields.php:150
    586594msgid "(GMT+01:00) Berlin"
    587595msgstr ""
    588596
    589 #: includes/class-kind-fields.php:139
     597#: includes/class-kind-fields.php:151
    590598msgid "(GMT+01:00) Brussels"
    591599msgstr ""
    592600
    593 #: includes/class-kind-fields.php:140
     601#: includes/class-kind-fields.php:152
    594602msgid "(GMT+01:00) Budapest"
    595603msgstr ""
    596604
    597 #: includes/class-kind-fields.php:141
     605#: includes/class-kind-fields.php:153
    598606msgid "(GMT+01:00) Copenhagen"
    599607msgstr ""
    600608
    601 #: includes/class-kind-fields.php:142
     609#: includes/class-kind-fields.php:154
    602610msgid "(GMT+01:00) Gibraltar"
    603611msgstr ""
    604612
    605 #: includes/class-kind-fields.php:143
     613#: includes/class-kind-fields.php:155
    606614msgid "(GMT+01:00) Luxembourg"
    607615msgstr ""
    608616
    609 #: includes/class-kind-fields.php:144
     617#: includes/class-kind-fields.php:156
    610618msgid "(GMT+01:00) Madrid"
    611619msgstr ""
    612620
    613 #: includes/class-kind-fields.php:145
     621#: includes/class-kind-fields.php:157
    614622msgid "(GMT+01:00) Malta"
    615623msgstr ""
    616624
    617 #: includes/class-kind-fields.php:146
     625#: includes/class-kind-fields.php:158
    618626msgid "(GMT+01:00) Monaco"
    619627msgstr ""
    620628
    621 #: includes/class-kind-fields.php:147
     629#: includes/class-kind-fields.php:159
    622630msgid "(GMT+01:00) Oslo"
    623631msgstr ""
    624632
    625 #: includes/class-kind-fields.php:148
     633#: includes/class-kind-fields.php:160
    626634msgid "(GMT+01:00) Paris"
    627635msgstr ""
    628636
    629 #: includes/class-kind-fields.php:149
     637#: includes/class-kind-fields.php:161
    630638msgid "(GMT+01:00) Central European Time - Prague"
    631639msgstr ""
    632640
    633 #: includes/class-kind-fields.php:150
     641#: includes/class-kind-fields.php:162
    634642msgid "(GMT+01:00) Rome"
    635643msgstr ""
    636644
    637 #: includes/class-kind-fields.php:151
     645#: includes/class-kind-fields.php:163
    638646msgid "(GMT+01:00) Stockholm"
    639647msgstr ""
    640648
    641 #: includes/class-kind-fields.php:152
     649#: includes/class-kind-fields.php:164
    642650msgid "(GMT+01:00) Tirane"
    643651msgstr ""
    644652
    645 #: includes/class-kind-fields.php:153
     653#: includes/class-kind-fields.php:165
    646654msgid "(GMT+01:00) Vienna"
    647655msgstr ""
    648656
    649 #: includes/class-kind-fields.php:154
     657#: includes/class-kind-fields.php:166
    650658msgid "(GMT+01:00) Warsaw"
    651659msgstr ""
    652660
    653 #: includes/class-kind-fields.php:155
     661#: includes/class-kind-fields.php:167
    654662msgid "(GMT+01:00) Zurich"
    655663msgstr ""
    656664
    657 #: includes/class-kind-fields.php:156
     665#: includes/class-kind-fields.php:168
    658666msgid "(GMT+02:00) Cairo"
    659667msgstr ""
    660668
    661 #: includes/class-kind-fields.php:157
     669#: includes/class-kind-fields.php:169
    662670msgid "(GMT+02:00) Johannesburg"
    663671msgstr ""
    664672
    665 #: includes/class-kind-fields.php:158
     673#: includes/class-kind-fields.php:170
    666674msgid "(GMT+02:00) Maputo"
    667675msgstr ""
    668676
    669 #: includes/class-kind-fields.php:159
     677#: includes/class-kind-fields.php:171
    670678msgid "(GMT+02:00) Tripoli"
    671679msgstr ""
    672680
    673 #: includes/class-kind-fields.php:160
     681#: includes/class-kind-fields.php:172
    674682msgid "(GMT+02:00) Amman"
    675683msgstr ""
    676684
    677 #: includes/class-kind-fields.php:161
     685#: includes/class-kind-fields.php:173
    678686msgid "(GMT+02:00) Beirut"
    679687msgstr ""
    680688
    681 #: includes/class-kind-fields.php:162
     689#: includes/class-kind-fields.php:174
    682690msgid "(GMT+02:00) Damascus"
    683691msgstr ""
    684692
    685 #: includes/class-kind-fields.php:163
     693#: includes/class-kind-fields.php:175
    686694msgid "(GMT+02:00) Gaza"
    687695msgstr ""
    688696
    689 #: includes/class-kind-fields.php:164
     697#: includes/class-kind-fields.php:176
    690698msgid "(GMT+02:00) Jerusalem"
    691699msgstr ""
    692700
    693 #: includes/class-kind-fields.php:165
     701#: includes/class-kind-fields.php:177
    694702msgid "(GMT+02:00) Nicosia"
    695703msgstr ""
    696704
    697 #: includes/class-kind-fields.php:166
     705#: includes/class-kind-fields.php:178
    698706msgid "(GMT+02:00) Athens"
    699707msgstr ""
    700708
    701 #: includes/class-kind-fields.php:167
     709#: includes/class-kind-fields.php:179
    702710msgid "(GMT+02:00) Bucharest"
    703711msgstr ""
    704712
    705 #: includes/class-kind-fields.php:168
     713#: includes/class-kind-fields.php:180
    706714msgid "(GMT+02:00) Chisinau"
    707715msgstr ""
    708716
    709 #: includes/class-kind-fields.php:169
     717#: includes/class-kind-fields.php:181
    710718msgid "(GMT+02:00) Helsinki"
    711719msgstr ""
    712720
    713 #: includes/class-kind-fields.php:170
     721#: includes/class-kind-fields.php:182
    714722msgid "(GMT+02:00) Istanbul"
    715723msgstr ""
    716724
    717 #: includes/class-kind-fields.php:171
     725#: includes/class-kind-fields.php:183
    718726msgid "(GMT+02:00) Moscow-01 - Kaliningrad"
    719727msgstr ""
    720728
    721 #: includes/class-kind-fields.php:172
     729#: includes/class-kind-fields.php:184
    722730msgid "(GMT+02:00) Kiev"
    723731msgstr ""
    724732
    725 #: includes/class-kind-fields.php:173
     733#: includes/class-kind-fields.php:185
    726734msgid "(GMT+02:00) Riga"
    727735msgstr ""
    728736
    729 #: includes/class-kind-fields.php:174
     737#: includes/class-kind-fields.php:186
    730738msgid "(GMT+02:00) Sofia"
    731739msgstr ""
    732740
    733 #: includes/class-kind-fields.php:175
     741#: includes/class-kind-fields.php:187
    734742msgid "(GMT+02:00) Tallinn"
    735743msgstr ""
    736744
    737 #: includes/class-kind-fields.php:176
     745#: includes/class-kind-fields.php:188
    738746msgid "(GMT+02:00) Vilnius"
    739747msgstr ""
    740748
    741 #: includes/class-kind-fields.php:177
     749#: includes/class-kind-fields.php:189
    742750msgid "(GMT+03:00) Khartoum"
    743751msgstr ""
    744752
    745 #: includes/class-kind-fields.php:178
     753#: includes/class-kind-fields.php:190
    746754msgid "(GMT+03:00) Nairobi"
    747755msgstr ""
    748756
    749 #: includes/class-kind-fields.php:179
     757#: includes/class-kind-fields.php:191
    750758msgid "(GMT+03:00) Syowa"
    751759msgstr ""
    752760
    753 #: includes/class-kind-fields.php:180
     761#: includes/class-kind-fields.php:192
    754762msgid "(GMT+03:00) Baghdad"
    755763msgstr ""
    756764
    757 #: includes/class-kind-fields.php:181
     765#: includes/class-kind-fields.php:193
    758766msgid "(GMT+03:00) Qatar"
    759767msgstr ""
    760768
    761 #: includes/class-kind-fields.php:182
     769#: includes/class-kind-fields.php:194
    762770msgid "(GMT+03:00) Riyadh"
    763771msgstr ""
    764772
    765 #: includes/class-kind-fields.php:183
     773#: includes/class-kind-fields.php:195
    766774msgid "(GMT+03:00) Minsk"
    767775msgstr ""
    768776
    769 #: includes/class-kind-fields.php:184
     777#: includes/class-kind-fields.php:196
    770778msgid "(GMT+03:00) Moscow+00 - Moscow"
    771779msgstr ""
    772780
    773 #: includes/class-kind-fields.php:185
     781#: includes/class-kind-fields.php:197
    774782msgid "(GMT+03:30) Tehran"
    775783msgstr ""
    776784
    777 #: includes/class-kind-fields.php:186
     785#: includes/class-kind-fields.php:198
    778786msgid "(GMT+04:00) Baku"
    779787msgstr ""
    780788
    781 #: includes/class-kind-fields.php:187
     789#: includes/class-kind-fields.php:199
    782790msgid "(GMT+04:00) Dubai"
    783791msgstr ""
    784792
    785 #: includes/class-kind-fields.php:188
     793#: includes/class-kind-fields.php:200
    786794msgid "(GMT+04:00) Tbilisi"
    787795msgstr ""
    788796
    789 #: includes/class-kind-fields.php:189
     797#: includes/class-kind-fields.php:201
    790798msgid "(GMT+04:00) Yerevan"
    791799msgstr ""
    792800
    793 #: includes/class-kind-fields.php:190
     801#: includes/class-kind-fields.php:202
    794802msgid "(GMT+04:00) Moscow+01 - Samara"
    795803msgstr ""
    796804
    797 #: includes/class-kind-fields.php:191
     805#: includes/class-kind-fields.php:203
    798806msgid "(GMT+04:00) Mahe"
    799807msgstr ""
    800808
    801 #: includes/class-kind-fields.php:192
     809#: includes/class-kind-fields.php:204
    802810msgid "(GMT+04:00) Mauritius"
    803811msgstr ""
    804812
    805 #: includes/class-kind-fields.php:193
     813#: includes/class-kind-fields.php:205
    806814msgid "(GMT+04:00) Reunion"
    807815msgstr ""
    808816
    809 #: includes/class-kind-fields.php:194
     817#: includes/class-kind-fields.php:206
    810818msgid "(GMT+04:30) Kabul"
    811819msgstr ""
    812820
    813 #: includes/class-kind-fields.php:195
     821#: includes/class-kind-fields.php:207
    814822msgid "(GMT+05:00) Mawson"
    815823msgstr ""
    816824
    817 #: includes/class-kind-fields.php:196
     825#: includes/class-kind-fields.php:208
    818826msgid "(GMT+05:00) Aqtau"
    819827msgstr ""
    820828
    821 #: includes/class-kind-fields.php:197
     829#: includes/class-kind-fields.php:209
    822830msgid "(GMT+05:00) Aqtobe"
    823831msgstr ""
    824832
    825 #: includes/class-kind-fields.php:198
     833#: includes/class-kind-fields.php:210
    826834msgid "(GMT+05:00) Ashgabat"
    827835msgstr ""
    828836
    829 #: includes/class-kind-fields.php:199
     837#: includes/class-kind-fields.php:211
    830838msgid "(GMT+05:00) Dushanbe"
    831839msgstr ""
    832840
    833 #: includes/class-kind-fields.php:200
     841#: includes/class-kind-fields.php:212
    834842msgid "(GMT+05:00) Karachi"
    835843msgstr ""
    836844
    837 #: includes/class-kind-fields.php:201
     845#: includes/class-kind-fields.php:213
    838846msgid "(GMT+05:00) Tashkent"
    839847msgstr ""
    840848
    841 #: includes/class-kind-fields.php:202
     849#: includes/class-kind-fields.php:214
    842850msgid "(GMT+05:00) Moscow+02 - Yekaterinburg"
    843851msgstr ""
    844852
    845 #: includes/class-kind-fields.php:203
     853#: includes/class-kind-fields.php:215
    846854msgid "(GMT+05:00) Kerguelen"
    847855msgstr ""
    848856
    849 #: includes/class-kind-fields.php:204
     857#: includes/class-kind-fields.php:216
    850858msgid "(GMT+05:00) Maldives"
    851859msgstr ""
    852860
    853 #: includes/class-kind-fields.php:205
     861#: includes/class-kind-fields.php:217
    854862msgid "(GMT+05:30) India Standard Time"
    855863msgstr ""
    856864
    857 #: includes/class-kind-fields.php:206
     865#: includes/class-kind-fields.php:218
    858866msgid "(GMT+05:30) Colombo"
    859867msgstr ""
    860868
    861 #: includes/class-kind-fields.php:207
     869#: includes/class-kind-fields.php:219
    862870msgid "(GMT+05:45) Katmandu"
    863871msgstr ""
    864872
    865 #: includes/class-kind-fields.php:208
     873#: includes/class-kind-fields.php:220
    866874msgid "(GMT+06:00) Vostok"
    867875msgstr ""
    868876
    869 #: includes/class-kind-fields.php:209
     877#: includes/class-kind-fields.php:221
    870878msgid "(GMT+06:00) Almaty"
    871879msgstr ""
    872880
    873 #: includes/class-kind-fields.php:210
     881#: includes/class-kind-fields.php:222
    874882msgid "(GMT+06:00) Bishkek"
    875883msgstr ""
    876884
    877 #: includes/class-kind-fields.php:211
     885#: includes/class-kind-fields.php:223
    878886msgid "(GMT+06:00) Dhaka"
    879887msgstr ""
    880888
    881 #: includes/class-kind-fields.php:212
     889#: includes/class-kind-fields.php:224
    882890msgid "(GMT+06:00) Moscow+03 - Omsk, Novosibirsk"
    883891msgstr ""
    884892
    885 #: includes/class-kind-fields.php:213
     893#: includes/class-kind-fields.php:225
    886894msgid "(GMT+06:00) Thimphu"
    887895msgstr ""
    888896
    889 #: includes/class-kind-fields.php:214
     897#: includes/class-kind-fields.php:226
    890898msgid "(GMT+06:00) Chagos"
    891899msgstr ""
    892900
    893 #: includes/class-kind-fields.php:215
     901#: includes/class-kind-fields.php:227
    894902msgid "(GMT+06:30) Rangoon"
    895903msgstr ""
    896904
    897 #: includes/class-kind-fields.php:216
     905#: includes/class-kind-fields.php:228
    898906msgid "(GMT+06:30) Cocos"
    899907msgstr ""
    900908
    901 #: includes/class-kind-fields.php:217
     909#: includes/class-kind-fields.php:229
    902910msgid "(GMT+07:00) Davis"
    903911msgstr ""
    904912
    905 #: includes/class-kind-fields.php:218
     913#: includes/class-kind-fields.php:230
    906914msgid "(GMT+07:00) Bangkok"
    907915msgstr ""
    908916
    909 #: includes/class-kind-fields.php:219
     917#: includes/class-kind-fields.php:231
    910918msgid "(GMT+07:00) Hovd"
    911919msgstr ""
    912920
    913 #: includes/class-kind-fields.php:220
     921#: includes/class-kind-fields.php:232
    914922msgid "(GMT+07:00) Jakarta"
    915923msgstr ""
    916924
    917 #: includes/class-kind-fields.php:221
     925#: includes/class-kind-fields.php:233
    918926msgid "(GMT+07:00) Moscow+04 - Krasnoyarsk"
    919927msgstr ""
    920928
    921 #: includes/class-kind-fields.php:222
     929#: includes/class-kind-fields.php:234
    922930msgid "(GMT+07:00) Hanoi"
    923931msgstr ""
    924932
    925 #: includes/class-kind-fields.php:223
     933#: includes/class-kind-fields.php:235
    926934msgid "(GMT+07:00) Ho Chi Minh"
    927935msgstr ""
    928936
    929 #: includes/class-kind-fields.php:224
     937#: includes/class-kind-fields.php:236
    930938msgid "(GMT+07:00) Christmas"
    931939msgstr ""
    932940
    933 #: includes/class-kind-fields.php:225
     941#: includes/class-kind-fields.php:237
    934942msgid "(GMT+08:00) Casey"
    935943msgstr ""
    936944
    937 #: includes/class-kind-fields.php:226
     945#: includes/class-kind-fields.php:238
    938946msgid "(GMT+08:00) Brunei"
    939947msgstr ""
    940948
    941 #: includes/class-kind-fields.php:227
     949#: includes/class-kind-fields.php:239
    942950msgid "(GMT+08:00) Choibalsan"
    943951msgstr ""
    944952
    945 #: includes/class-kind-fields.php:228
     953#: includes/class-kind-fields.php:240
    946954msgid "(GMT+08:00) Hong Kong"
    947955msgstr ""
    948956
    949 #: includes/class-kind-fields.php:229
     957#: includes/class-kind-fields.php:241
    950958msgid "(GMT+08:00) Moscow+05 - Irkutsk"
    951959msgstr ""
    952960
    953 #: includes/class-kind-fields.php:230
     961#: includes/class-kind-fields.php:242
    954962msgid "(GMT+08:00) Kuala Lumpur"
    955963msgstr ""
    956964
    957 #: includes/class-kind-fields.php:231
     965#: includes/class-kind-fields.php:243
    958966msgid "(GMT+08:00) Macau"
    959967msgstr ""
    960968
    961 #: includes/class-kind-fields.php:232
     969#: includes/class-kind-fields.php:244
    962970msgid "(GMT+08:00) Makassar"
    963971msgstr ""
    964972
    965 #: includes/class-kind-fields.php:233
     973#: includes/class-kind-fields.php:245
    966974msgid "(GMT+08:00) Manila"
    967975msgstr ""
    968976
    969 #: includes/class-kind-fields.php:234
     977#: includes/class-kind-fields.php:246
    970978msgid "(GMT+08:00) China Time - Beijing"
    971979msgstr ""
    972980
    973 #: includes/class-kind-fields.php:235
     981#: includes/class-kind-fields.php:247
    974982msgid "(GMT+08:00) Singapore"
    975983msgstr ""
    976984
    977 #: includes/class-kind-fields.php:236
     985#: includes/class-kind-fields.php:248
    978986msgid "(GMT+08:00) Taipei"
    979987msgstr ""
    980988
    981 #: includes/class-kind-fields.php:237
     989#: includes/class-kind-fields.php:249
    982990msgid "(GMT+08:00) Ulaanbaatar"
    983991msgstr ""
    984992
    985 #: includes/class-kind-fields.php:238
     993#: includes/class-kind-fields.php:250
    986994msgid "(GMT+08:00) Western Time - Perth"
    987995msgstr ""
    988996
    989 #: includes/class-kind-fields.php:239
     997#: includes/class-kind-fields.php:251
    990998msgid "(GMT+08:30) Pyongyang"
    991999msgstr ""
    9921000
    993 #: includes/class-kind-fields.php:240
     1001#: includes/class-kind-fields.php:252
    9941002msgid "(GMT+09:00) Dili"
    9951003msgstr ""
    9961004
    997 #: includes/class-kind-fields.php:241
     1005#: includes/class-kind-fields.php:253
    9981006msgid "(GMT+09:00) Jayapura"
    9991007msgstr ""
    10001008
    1001 #: includes/class-kind-fields.php:242
     1009#: includes/class-kind-fields.php:254
    10021010msgid "(GMT+09:00) Seoul"
    10031011msgstr ""
    10041012
    1005 #: includes/class-kind-fields.php:243
     1013#: includes/class-kind-fields.php:255
    10061014msgid "(GMT+09:00) Tokyo"
    10071015msgstr ""
    10081016
    1009 #: includes/class-kind-fields.php:244
     1017#: includes/class-kind-fields.php:256
    10101018msgid "(GMT+09:00) Moscow+06 - Yakutsk"
    10111019msgstr ""
    10121020
    1013 #: includes/class-kind-fields.php:245
     1021#: includes/class-kind-fields.php:257
    10141022msgid "(GMT+09:00) Palau"
    10151023msgstr ""
    10161024
    1017 #: includes/class-kind-fields.php:246
     1025#: includes/class-kind-fields.php:258
    10181026msgid "(GMT+10:30) Central Time - Adelaide"
    10191027msgstr ""
    10201028
    1021 #: includes/class-kind-fields.php:247
     1029#: includes/class-kind-fields.php:259
    10221030msgid "(GMT+09:30) Central Time - Darwin"
    10231031msgstr ""
    10241032
    1025 #: includes/class-kind-fields.php:249
     1033#: includes/class-kind-fields.php:261
    10261034msgid "(GMT+10:00) Moscow+07 - Magadan"
    10271035msgstr ""
    10281036
    1029 #: includes/class-kind-fields.php:250
     1037#: includes/class-kind-fields.php:262
    10301038msgid "(GMT+10:00) Moscow+07 - Yuzhno-Sakhalinsk"
    10311039msgstr ""
    10321040
    1033 #: includes/class-kind-fields.php:251
     1041#: includes/class-kind-fields.php:263
    10341042msgid "(GMT+10:00) Eastern Time - Brisbane"
    10351043msgstr ""
    10361044
    1037 #: includes/class-kind-fields.php:252
     1045#: includes/class-kind-fields.php:264
    10381046msgid "(GMT+11:00) Eastern Time - Hobart"
    10391047msgstr ""
    10401048
    1041 #: includes/class-kind-fields.php:253
     1049#: includes/class-kind-fields.php:265
    10421050msgid "(GMT+11:00) Eastern Time - Melbourne, Sydney"
    10431051msgstr ""
    10441052
    1045 #: includes/class-kind-fields.php:254
     1053#: includes/class-kind-fields.php:266
    10461054msgid "(GMT+10:00) Truk"
    10471055msgstr ""
    10481056
    1049 #: includes/class-kind-fields.php:255
     1057#: includes/class-kind-fields.php:267
    10501058msgid "(GMT+10:00) Guam"
    10511059msgstr ""
    10521060
    1053 #: includes/class-kind-fields.php:256
     1061#: includes/class-kind-fields.php:268
    10541062msgid "(GMT+10:00) Port Moresby"
    10551063msgstr ""
    10561064
    1057 #: includes/class-kind-fields.php:257
     1065#: includes/class-kind-fields.php:269
    10581066msgid "(GMT+11:00) Efate"
    10591067msgstr ""
    10601068
    1061 #: includes/class-kind-fields.php:258
     1069#: includes/class-kind-fields.php:270
    10621070msgid "(GMT+11:00) Guadalcanal"
    10631071msgstr ""
    10641072
    1065 #: includes/class-kind-fields.php:259
     1073#: includes/class-kind-fields.php:271
    10661074msgid "(GMT+11:00) Kosrae"
    10671075msgstr ""
    10681076
    1069 #: includes/class-kind-fields.php:260
     1077#: includes/class-kind-fields.php:272
    10701078msgid "(GMT+11:00) Norfolk"
    10711079msgstr ""
    10721080
    1073 #: includes/class-kind-fields.php:261
     1081#: includes/class-kind-fields.php:273
    10741082msgid "(GMT+11:00) Noumea"
    10751083msgstr ""
    10761084
    1077 #: includes/class-kind-fields.php:262
     1085#: includes/class-kind-fields.php:274
    10781086msgid "(GMT+11:00) Ponape"
    10791087msgstr ""
    10801088
    1081 #: includes/class-kind-fields.php:263
     1089#: includes/class-kind-fields.php:275
    10821090msgid "(GMT+12:00) Moscow+09 - Petropavlovsk-Kamchatskiy"
    10831091msgstr ""
    10841092
    1085 #: includes/class-kind-fields.php:264
     1093#: includes/class-kind-fields.php:276
    10861094msgid "(GMT+13:00) Auckland"
    10871095msgstr ""
    10881096
    1089 #: includes/class-kind-fields.php:265
     1097#: includes/class-kind-fields.php:277
    10901098msgid "(GMT+13:00) Fiji"
    10911099msgstr ""
    10921100
    1093 #: includes/class-kind-fields.php:266
     1101#: includes/class-kind-fields.php:278
    10941102msgid "(GMT+12:00) Funafuti"
    10951103msgstr ""
    10961104
    1097 #: includes/class-kind-fields.php:267
     1105#: includes/class-kind-fields.php:279
    10981106msgid "(GMT+12:00) Kwajalein"
    10991107msgstr ""
    11001108
    1101 #: includes/class-kind-fields.php:268
     1109#: includes/class-kind-fields.php:280
    11021110msgid "(GMT+12:00) Majuro"
    11031111msgstr ""
    11041112
    1105 #: includes/class-kind-fields.php:269
     1113#: includes/class-kind-fields.php:281
    11061114msgid "(GMT+12:00) Nauru"
    11071115msgstr ""
    11081116
    1109 #: includes/class-kind-fields.php:270
     1117#: includes/class-kind-fields.php:282
    11101118msgid "(GMT+12:00) Tarawa"
    11111119msgstr ""
    11121120
    1113 #: includes/class-kind-fields.php:271
     1121#: includes/class-kind-fields.php:283
    11141122msgid "(GMT+12:00) Wake"
    11151123msgstr ""
    11161124
    1117 #: includes/class-kind-fields.php:272
     1125#: includes/class-kind-fields.php:284
    11181126msgid "(GMT+12:00) Wallis"
    11191127msgstr ""
    11201128
    1121 #: includes/class-kind-fields.php:273
     1129#: includes/class-kind-fields.php:285
    11221130msgid "(GMT+14:00) Apia"
    11231131msgstr ""
    11241132
    1125 #: includes/class-kind-fields.php:274
     1133#: includes/class-kind-fields.php:286
    11261134msgid "(GMT+13:00) Enderbury"
    11271135msgstr ""
    11281136
    1129 #: includes/class-kind-fields.php:275
     1137#: includes/class-kind-fields.php:287
    11301138msgid "(GMT+13:00) Fakaofo"
    11311139msgstr ""
    11321140
    1133 #: includes/class-kind-fields.php:276
     1141#: includes/class-kind-fields.php:288
    11341142msgid "(GMT+13:00) Tongatapu"
    11351143msgstr ""
    11361144
    1137 #: includes/class-kind-fields.php:277
     1145#: includes/class-kind-fields.php:289
    11381146msgid "(GMT+14:00) Kiritimati"
    11391147msgstr ""
    11401148
    1141 #: includes/class-kind-fields.php:385
     1149#: includes/class-kind-fields.php:396
    11421150msgid "Author Name"
    11431151msgstr ""
    11441152
    1145 #: includes/class-kind-fields.php:389
    1146 #: templates/reply-author.php:12
     1153#: includes/class-kind-fields.php:400
     1154#: templates/reply-author.php:26
    11471155msgid "Author URL"
    11481156msgstr ""
    11491157
    1150 #: includes/class-kind-fields.php:393
     1158#: includes/class-kind-fields.php:404
    11511159msgid "Author Photo"
    11521160msgstr ""
    11531161
    1154 #: includes/class-kind-media-metadata.php:66
    1155 #: includes/register-kinds.php:542
    1156 #: includes/register-kinds.php:607
     1162#: includes/class-kind-media-metadata.php:125
     1163#: includes/register-kinds.php:551
     1164#: includes/register-kinds.php:616
    11571165msgid "Artist"
    11581166msgstr ""
    11591167
    1160 #: includes/class-kind-media-metadata.php:67
     1168#: includes/class-kind-media-metadata.php:126
    11611169msgid "The creator of the image"
    11621170msgstr ""
    11631171
    1164 #: includes/class-kind-media-metadata.php:72
     1172#: includes/class-kind-media-metadata.php:131
    11651173msgid "Artist URL"
    11661174msgstr ""
    11671175
    1168 #: includes/class-kind-media-metadata.php:73
     1176#: includes/class-kind-media-metadata.php:132
    11691177msgid "The Artists URL or webpage"
    11701178msgstr ""
    11711179
    1172 #: includes/class-kind-menu-widget.php:10
     1180#: includes/class-kind-menu-widget.php:25
    11731181msgid "Kind Menu Widget"
    11741182msgstr ""
    11751183
    1176 #: includes/class-kind-menu-widget.php:13
     1184#: includes/class-kind-menu-widget.php:28
    11771185msgid "A widget that allows you to display a menu of kind archives"
    11781186msgstr ""
    11791187
    11801188#. translators: 1. Blog Title 2. Kind Singular Name
    1181 #: includes/class-kind-menu-widget.php:62
    1182 msgid "title=\"%1$s %2$s Feed\""
    1183 msgstr ""
    1184 
    1185 #: includes/class-kind-menu-widget.php:78
     1189#: includes/class-kind-menu-widget.php:80
     1190#, php-format
     1191msgid "%1$s %2$s Feed"
     1192msgstr ""
     1193
     1194#: includes/class-kind-menu-widget.php:96
    11861195msgid "All Posts"
    11871196msgstr ""
    11881197
    1189 #: includes/class-kind-menu-widget.php:122
    1190 #: includes/class-kind-post-widget.php:95
     1198#: includes/class-kind-menu-widget.php:149
     1199#: includes/class-kind-post-widget.php:118
    11911200msgid "Title: "
    11921201msgstr ""
    11931202
    1194 #: includes/class-kind-menu-widget.php:143
     1203#: includes/class-kind-menu-widget.php:169
    11951204msgid "Show Link to All:"
    11961205msgstr ""
    11971206
    1198 #: includes/class-kind-menu-widget.php:148
     1207#: includes/class-kind-menu-widget.php:174
    11991208msgid "Show Count:"
    12001209msgstr ""
    12011210
    1202 #: includes/class-kind-metabox.php:154
     1211#: includes/class-kind-metabox.php:211
    12031212msgid "Your URL has been successfully retrieved and parsed"
    12041213msgstr ""
    12051214
    1206 #: includes/class-kind-metabox.php:155
     1215#: includes/class-kind-metabox.php:212
    12071216msgid "Are you sure you want to clear post properties?"
    12081217msgstr ""
    12091218
    1210 #: includes/class-kind-metabox.php:272
     1219#: includes/class-kind-metabox.php:213
     1220msgid "Error: Unable to Retrieve"
     1221msgstr ""
     1222
     1223#: includes/class-kind-metabox.php:214
     1224msgid "Invalid URL"
     1225msgstr ""
     1226
     1227#: includes/class-kind-metabox.php:215
     1228msgid "Attach"
     1229msgstr ""
     1230
     1231#: includes/class-kind-metabox.php:216
     1232msgid "Use this media"
     1233msgstr ""
     1234
     1235#: includes/class-kind-metabox.php:346
    12111236msgid "Very Dissatisfied"
    12121237msgstr ""
    12131238
    1214 #: includes/class-kind-metabox.php:273
     1239#: includes/class-kind-metabox.php:347
    12151240msgid "Dissatisfied"
    12161241msgstr ""
    12171242
    1218 #: includes/class-kind-metabox.php:274
     1243#: includes/class-kind-metabox.php:348
    12191244msgid "Neutral"
    12201245msgstr ""
    12211246
    1222 #: includes/class-kind-metabox.php:275
     1247#: includes/class-kind-metabox.php:349
    12231248msgid "Satisfied"
    12241249msgstr ""
    12251250
    1226 #: includes/class-kind-metabox.php:276
     1251#: includes/class-kind-metabox.php:350
    12271252msgid "Very Satisfied"
    12281253msgstr ""
    12291254
    1230 #: includes/class-kind-metabox.php:300
    1231 #: includes/register-kinds.php:459
     1255#: includes/class-kind-metabox.php:376
     1256#: includes/register-kinds.php:468
    12321257msgid "Yes"
    12331258msgstr ""
    12341259
    1235 #: includes/class-kind-metabox.php:301
     1260#: includes/class-kind-metabox.php:377
     1261#: includes/register-kinds.php:469
     1262msgid "No"
     1263msgstr ""
     1264
     1265#: includes/class-kind-metabox.php:378
     1266#: includes/register-kinds.php:470
     1267msgid "Maybe"
     1268msgstr ""
     1269
     1270#: includes/class-kind-metabox.php:379
     1271#: includes/register-kinds.php:471
     1272msgid "Interested"
     1273msgstr ""
     1274
     1275#: includes/class-kind-metabox.php:380
     1276#: includes/register-kinds.php:472
     1277msgid "Remote"
     1278msgstr ""
     1279
     1280#: includes/class-kind-metabox.php:404
    12361281#: includes/register-kinds.php:460
    1237 msgid "No"
    1238 msgstr ""
    1239 
    1240 #: includes/class-kind-metabox.php:302
    1241 #: includes/register-kinds.php:461
    1242 msgid "Maybe"
    1243 msgstr ""
    1244 
    1245 #: includes/class-kind-metabox.php:303
    1246 #: includes/register-kinds.php:462
    1247 msgid "Interested"
    1248 msgstr ""
    1249 
    1250 #: includes/class-kind-metabox.php:304
    1251 #: includes/register-kinds.php:463
    1252 msgid "Remote"
    1253 msgstr ""
    1254 
    1255 #: includes/class-kind-metabox.php:326
    1256 #: includes/register-kinds.php:451
     1282#: views/kind.php:81
    12571283msgid "RSVP"
    12581284msgstr ""
    12591285
    1260 #: includes/class-kind-metabox.php:342
     1286#: includes/class-kind-metabox.php:422
    12611287msgid "Rating"
    12621288msgstr ""
    12631289
    1264 #: includes/class-kind-metabox.php:357
     1290#: includes/class-kind-metabox.php:461
    12651291msgid "Response Properties"
    12661292msgstr ""
    12671293
    1268 #: includes/class-kind-plugins.php:133
     1294#: includes/class-kind-plugins.php:209
    12691295msgctxt "direct article"
    12701296msgid "this"
    12711297msgstr ""
    12721298
    1273 #: includes/class-kind-post-widget.php:10
     1299#: includes/class-kind-plugins.php:535
     1300msgid "Nothing could be parsed from the URL."
     1301msgstr ""
     1302
     1303#: includes/class-kind-post-widget.php:25
    12741304msgid "Kind Post Widget"
    12751305msgstr ""
    12761306
    1277 #: includes/class-kind-post-widget.php:13
     1307#: includes/class-kind-post-widget.php:28
    12781308msgid "A widget that allows you to display a list of posts by type"
    12791309msgstr ""
    12801310
    1281 #: includes/class-kind-post-widget.php:63
    1282 msgid "No Posts Found"
    1283 msgstr ""
    1284 
    1285 #: includes/class-kind-post-widget.php:115
     1311#: includes/class-kind-post-widget.php:137
    12861312msgid "Number of Posts:"
    12871313msgstr ""
    12881314
    1289 #: includes/class-kind-taxonomy.php:305
    1290 msgid "Post Kind"
    1291 msgstr ""
    1292 
    1293 #: includes/class-kind-taxonomy.php:526
     1315#: includes/class-kind-taxonomy.php:161
     1316msgid "Unknown post kind."
     1317msgstr ""
     1318
     1319#: includes/class-kind-taxonomy.php:363
     1320msgid "The post kind, or false if the post has none."
     1321msgstr ""
     1322
     1323#: includes/class-kind-taxonomy.php:609
     1324msgid "Post Kinds replaces the IndieWeb Taxonomy plugin. Deactivate IndieWeb Taxonomy, then activate Post Kinds."
     1325msgstr ""
     1326
     1327#: includes/class-kind-taxonomy.php:697
    12941328msgctxt "taxonomy general name"
    12951329msgid "Kinds"
    12961330msgstr ""
    12971331
    1298 #: includes/class-kind-taxonomy.php:527
     1332#: includes/class-kind-taxonomy.php:698
    12991333msgctxt "taxonomy singular name"
    13001334msgid "Kind"
    13011335msgstr ""
    13021336
    1303 #: includes/class-kind-taxonomy.php:528
     1337#: includes/class-kind-taxonomy.php:699
    13041338msgctxt "search locations"
    13051339msgid "Search Kinds"
    13061340msgstr ""
    13071341
    1308 #: includes/class-kind-taxonomy.php:529
     1342#: includes/class-kind-taxonomy.php:700
    13091343msgctxt "popular kinds"
    13101344msgid "Popular Kinds"
    13111345msgstr ""
    13121346
    1313 #: includes/class-kind-taxonomy.php:530
     1347#: includes/class-kind-taxonomy.php:701
    13141348msgctxt "all taxonomy items"
    13151349msgid "All Kinds"
    13161350msgstr ""
    13171351
    1318 #: includes/class-kind-taxonomy.php:531
     1352#: includes/class-kind-taxonomy.php:702
    13191353msgctxt "taxonomy parent item"
    13201354msgid "Parent Kind"
    13211355msgstr ""
    13221356
    1323 #: includes/class-kind-taxonomy.php:532
     1357#: includes/class-kind-taxonomy.php:703
    13241358msgctxt "taxonomy parent item with colon"
    13251359msgid "Parent Kind:"
    13261360msgstr ""
    13271361
    1328 #: includes/class-kind-taxonomy.php:533
     1362#: includes/class-kind-taxonomy.php:704
    13291363msgid "This will appear on archives as the description if your theme supports"
    13301364msgstr ""
    13311365
    1332 #: includes/class-kind-taxonomy.php:534
     1366#: includes/class-kind-taxonomy.php:705
    13331367msgctxt "edit taxonomy item"
    13341368msgid "Edit Kind"
    13351369msgstr ""
    13361370
    1337 #: includes/class-kind-taxonomy.php:535
     1371#: includes/class-kind-taxonomy.php:706
    13381372msgctxt "view taxonomy item"
    13391373msgid "View Kind"
    13401374msgstr ""
    13411375
    1342 #: includes/class-kind-taxonomy.php:536
     1376#: includes/class-kind-taxonomy.php:707
    13431377msgctxt "update taxonomy item"
    13441378msgid "Update Kind"
    13451379msgstr ""
    13461380
    1347 #: includes/class-kind-taxonomy.php:537
     1381#: includes/class-kind-taxonomy.php:708
    13481382msgctxt "add taxonomy item"
    13491383msgid "Add New Kind"
    13501384msgstr ""
    13511385
    1352 #: includes/class-kind-taxonomy.php:538
     1386#: includes/class-kind-taxonomy.php:709
    13531387msgctxt "new taxonomy item"
    13541388msgid "New Kind"
    13551389msgstr ""
    13561390
    1357 #: includes/class-kind-taxonomy.php:539
     1391#: includes/class-kind-taxonomy.php:710
    13581392msgctxt "separate kinds with commas"
    13591393msgid "Separate kinds with commas"
    13601394msgstr ""
    13611395
    1362 #: includes/class-kind-taxonomy.php:540
     1396#: includes/class-kind-taxonomy.php:711
    13631397msgctxt "add or remove items"
    13641398msgid "Add or remove kinds"
    13651399msgstr ""
    13661400
    1367 #: includes/class-kind-taxonomy.php:541
     1401#: includes/class-kind-taxonomy.php:712
    13681402msgctxt "choose most used"
    13691403msgid "Choose from the most used kinds"
    13701404msgstr ""
    13711405
    1372 #: includes/class-kind-taxonomy.php:542
     1406#: includes/class-kind-taxonomy.php:713
    13731407msgctxt "no kinds found"
    13741408msgid "No kinds found"
    13751409msgstr ""
    13761410
    1377 #: includes/class-kind-taxonomy.php:543
     1411#: includes/class-kind-taxonomy.php:714
    13781412msgctxt "no kinds"
    13791413msgid "No kinds"
    13801414msgstr ""
    13811415
    1382 #: includes/class-kind-taxonomy.php:544
     1416#: includes/class-kind-taxonomy.php:715
    13831417msgid "Back to Kinds"
    13841418msgstr ""
    13851419
    1386 #: includes/class-kind-taxonomy.php:545
     1420#: includes/class-kind-taxonomy.php:716
    13871421msgid "Kind Link"
    13881422msgstr ""
    13891423
    1390 #: includes/class-kind-taxonomy.php:546
     1424#: includes/class-kind-taxonomy.php:717
    13911425msgid "A link to a kind"
    1392 msgstr ""
    1393 
    1394 #: includes/class-kind-taxonomy.php:565
    1395 #: includes/register-kinds.php:8
    1396 msgid "Article"
    13971426msgstr ""
    13981427
    … …  
    14001429#. translators: 1: Taxonomy. 2: Month name and year
    14011430#. translators: 1. Taxonomy . 1: Date
    1402 #: includes/class-kind-taxonomy.php:860
    1403 #: includes/class-kind-taxonomy.php:863
    1404 #: includes/class-kind-taxonomy.php:866
     1431#: includes/class-kind-taxonomy.php:1114
     1432#: includes/class-kind-taxonomy.php:1117
     1433#: includes/class-kind-taxonomy.php:1120
     1434#, php-format
    14051435msgid "%1$1s - %2$2s"
    14061436msgstr ""
    14071437
    14081438#. translators: 1: Taxonomy. Yearly archive title. 2: Year
    1409 #: includes/class-kind-taxonomy.php:860
     1439#: includes/class-kind-taxonomy.php:1114
    14101440msgctxt "yearly archives date format"
    14111441msgid "Y"
    … …  
    14131443
    14141444#. translators: 1: Taxonomy. 2: Month name and year
    1415 #: includes/class-kind-taxonomy.php:863
     1445#: includes/class-kind-taxonomy.php:1117
    14161446msgctxt "monthly archives date format"
    14171447msgid "F Y"
    … …  
    14191449
    14201450#. translators: 1. Taxonomy . 1: Date
    1421 #: includes/class-kind-taxonomy.php:866
     1451#: includes/class-kind-taxonomy.php:1120
    14221452msgctxt "daily archives date format"
    14231453msgid "F j, Y"
    … …  
    14251455
    14261456#. translators: 1. Title 2. Translated term for Firehose Feed
    1427 #: includes/class-kind-taxonomy.php:870
     1457#: includes/class-kind-taxonomy.php:1124
     1458#, php-format
    14281459msgid "%1$s - %2$2s"
    14291460msgstr ""
    14301461
    14311462#. translators: 1. Title 2. Translated term for Firehose Feed
    1432 #: includes/class-kind-taxonomy.php:870
     1463#: includes/class-kind-taxonomy.php:1124
    14331464msgid "Firehose"
    14341465msgstr ""
    14351466
    14361467#. translators: 1: Title prefix. 2: Title.
    1437 #: includes/class-kind-taxonomy.php:876
     1468#: includes/class-kind-taxonomy.php:1130
     1469#, php-format
    14381470msgctxt "archive title"
    14391471msgid "%1$s %2$s"
    … …  
    14411473
    14421474#. translators: All
    1443 #: includes/class-kind-taxonomy.php:1164
     1475#: includes/class-kind-taxonomy.php:1455
     1476#, php-format
    14441477msgid "All %1s"
    14451478msgstr ""
    14461479
    1447 #: includes/class-kind-taxonomy.php:1334
     1480#: includes/class-kind-taxonomy.php:1647
    14481481msgid "Invalid post"
    14491482msgstr ""
    14501483
    1451 #: includes/class-kind-taxonomy.php:1337
     1484#: includes/class-kind-taxonomy.php:1650
    14521485msgid "Invalid Kind"
    14531486msgstr ""
    14541487
    1455 #: includes/class-kind-view.php:418
     1488#: includes/class-kind-taxonomy.php:1670
     1489msgid "Invalid post."
     1490msgstr ""
     1491
     1492#: includes/class-kind-taxonomy.php:1673
     1493msgid "Sorry, you are not allowed to set the kind of this post."
     1494msgstr ""
     1495
     1496#: includes/class-kind-view.php:495
    14561497msgctxt "singular Twitter"
    14571498msgid "a tweet"
    14581499msgstr ""
    14591500
    1460 #: includes/class-kind-view.php:419
     1501#: includes/class-kind-view.php:496
    14611502msgctxt "singular Vimeo"
    14621503msgid "a video"
    14631504msgstr ""
    14641505
    1465 #: includes/class-kind-view.php:420
     1506#: includes/class-kind-view.php:497
    14661507msgctxt "singular Youtube"
    14671508msgid "a video"
    14681509msgstr ""
    14691510
    1470 #: includes/class-kind-view.php:421
     1511#: includes/class-kind-view.php:498
    14711512msgctxt "singular Intagram"
    14721513msgid "an image"
    14731514msgstr ""
    14741515
    1475 #: includes/class-kind-view.php:427
     1516#: includes/class-kind-view.php:513
    14761517msgctxt "singular post"
    14771518msgid "a post"
    14781519msgstr ""
    14791520
    1480 #: includes/class-kind-view.php:494
     1521#: includes/class-kind-view.php:589
    14811522msgid "an author"
    14821523msgstr ""
    14831524
    1484 #. translators: URL for link to event and name of event
    1485 #: includes/class-kind-view.php:614
    1486 msgid "Attending <a href=\"%1$s\" class=\"u-in-reply-to\">%2$s</a>"
    1487 msgstr ""
    1488 
    1489 #. translators: URL for link to event and name of event
    1490 #: includes/class-kind-view.php:616
    1491 msgid "Might be attending <a href=\"%1$s\" class=\"u-in-reply-to\">%2$s</a>"
    1492 msgstr ""
    1493 
    1494 #. translators: URL for link to event and name of event
    1495 #: includes/class-kind-view.php:618
    1496 msgid "Unable to Attend <a href=\"%1$s\" class=\"u-in-reply-to\">%2$s</a>"
    1497 msgstr ""
    1498 
    1499 #. translators: URL for link to event and name of event
    1500 #: includes/class-kind-view.php:620
    1501 msgid "Interested in Attending <a href=\"%1$s\" class=u-in-reply-to\">%2$s</a>"
    1502 msgstr ""
    1503 
    1504 #. translators: URL for link to event and name of event
    1505 #: includes/class-kind-view.php:622
    1506 msgid "Attending <a href=\"%1$s\" class=\"u-in-reply-to\">%2$s</a> remotely"
    1507 msgstr ""
    1508 
    1509 #: includes/class-kind-view.php:640
     1525#. translators: %s: Link to the event
     1526#: includes/class-kind-view.php:752
     1527#, php-format
     1528msgid "Attending %s"
     1529msgstr ""
     1530
     1531#. translators: %s: Link to the event
     1532#: includes/class-kind-view.php:754
     1533#, php-format
     1534msgid "Might be attending %s"
     1535msgstr ""
     1536
     1537#. translators: %s: Link to the event
     1538#: includes/class-kind-view.php:756
     1539#, php-format
     1540msgid "Unable to Attend %s"
     1541msgstr ""
     1542
     1543#. translators: %s: Link to the event
     1544#: includes/class-kind-view.php:758
     1545#, php-format
     1546msgid "Interested in Attending %s"
     1547msgstr ""
     1548
     1549#. translators: %s: Link to the event
     1550#: includes/class-kind-view.php:760
     1551#, php-format
     1552msgid "Attending %s remotely"
     1553msgstr ""
     1554
     1555#: includes/class-kind-view.php:794
    15101556msgid "Want to Read: "
    15111557msgstr ""
    15121558
    1513 #: includes/class-kind-view.php:641
     1559#: includes/class-kind-view.php:795
    15141560msgid "Reading: "
    15151561msgstr ""
    15161562
    1517 #: includes/class-kind-view.php:642
     1563#: includes/class-kind-view.php:796
    15181564msgid "Finished Reading: "
    15191565msgstr ""
    15201566
    15211567#. translators: singular and plural
    1522 #: includes/class-kind-view.php:674
     1568#: includes/class-kind-view.php:828
     1569#, php-format
    15231570msgid "%d year"
    15241571msgid_plural "%d years"
    … …  
    15271574
    15281575#. translators: singular and plural
    1529 #: includes/class-kind-view.php:678
     1576#: includes/class-kind-view.php:832
     1577#, php-format
    15301578msgid "%d month"
    15311579msgid_plural "%d months"
    … …  
    15341582
    15351583#. translators: singular and plural
    1536 #: includes/class-kind-view.php:682
     1584#: includes/class-kind-view.php:836
     1585#, php-format
    15371586msgid "%d day"
    15381587msgid_plural "%d days"
    … …  
    15411590
    15421591#. translators: singular and plural
    1543 #: includes/class-kind-view.php:686
     1592#: includes/class-kind-view.php:840
     1593#, php-format
    15441594msgid "%d hour"
    15451595msgid_plural "%d hours"
    … …  
    15481598
    15491599#. translators: singular and plural
    1550 #: includes/class-kind-view.php:690
     1600#: includes/class-kind-view.php:844
     1601#, php-format
    15511602msgid "%d minute"
    15521603msgid_plural "%d minutes"
    … …  
    15551606
    15561607#. translators: singular and plural
    1557 #: includes/class-kind-view.php:694
     1608#: includes/class-kind-view.php:848
     1609#, php-format
    15581610msgid "%d second"
    15591611msgid_plural "%d seconds"
    … …  
    15611613msgstr[1] ""
    15621614
    1563 #: includes/register-kinds.php:9
     1615#: includes/register-kinds.php:17
     1616msgid "Article"
     1617msgstr ""
     1618
     1619#: includes/register-kinds.php:18
    15641620msgid "Articles"
    15651621msgstr ""
    15661622
    1567 #: includes/register-kinds.php:14
     1623#: includes/register-kinds.php:23
    15681624msgid "traditional long form content: a post with an explicit title and body"
    15691625msgstr ""
    15701626
    1571 #: includes/register-kinds.php:25
     1627#: includes/register-kinds.php:34
    15721628msgid "Note"
    15731629msgstr ""
    15741630
    1575 #: includes/register-kinds.php:26
     1631#: includes/register-kinds.php:35
    15761632msgid "Notes"
    15771633msgstr ""
    15781634
    1579 #: includes/register-kinds.php:31
     1635#: includes/register-kinds.php:40
    15801636msgid "short content: a post or status update with just plain content and typically without a title"
    15811637msgstr ""
    15821638
    1583 #: includes/register-kinds.php:42
     1639#: includes/register-kinds.php:51
    15841640msgid "Reply"
    15851641msgstr ""
    15861642
    1587 #: includes/register-kinds.php:43
     1643#: includes/register-kinds.php:52
    15881644msgid "Replies"
    15891645msgstr ""
    15901646
    1591 #: includes/register-kinds.php:44
     1647#: includes/register-kinds.php:53
    15921648msgid "Replied to"
    15931649msgstr ""
    15941650
    1595 #: includes/register-kinds.php:49
     1651#: includes/register-kinds.php:58
    15961652msgid "In Reply To"
    15971653msgstr ""
    15981654
    1599 #: includes/register-kinds.php:53
     1655#: includes/register-kinds.php:62
     1656#: includes/register-kinds.php:131
     1657#: includes/register-kinds.php:200
     1658#: includes/register-kinds.php:269
     1659#: includes/register-kinds.php:338
     1660#: includes/register-kinds.php:481
     1661#: includes/register-kinds.php:543
     1662#: includes/register-kinds.php:608
     1663#: includes/register-kinds.php:673
     1664#: templates/reply-metabox.php:56
     1665msgid "Name"
     1666msgstr ""
     1667
     1668#: includes/register-kinds.php:66
     1669#: includes/register-kinds.php:135
     1670#: includes/register-kinds.php:204
     1671#: includes/register-kinds.php:273
     1672#: includes/register-kinds.php:342
     1673#: includes/register-kinds.php:485
     1674#: includes/register-kinds.php:547
     1675#: includes/register-kinds.php:612
     1676#: includes/register-kinds.php:677
     1677#: templates/reply-metabox.php:50
     1678msgid "URL"
     1679msgstr ""
     1680
     1681#: includes/register-kinds.php:70
     1682#: includes/register-kinds.php:139
     1683#: includes/register-kinds.php:208
     1684#: includes/register-kinds.php:277
     1685#: includes/register-kinds.php:346
     1686#: templates/reply-author.php:20
     1687#: templates/reply-metabox.php:45
     1688msgid "Author"
     1689msgstr ""
     1690
     1691#: includes/register-kinds.php:77
     1692#: includes/register-kinds.php:146
     1693#: includes/register-kinds.php:215
     1694#: includes/register-kinds.php:284
     1695#: includes/register-kinds.php:353
     1696#: includes/register-kinds.php:489
     1697#: includes/register-kinds.php:558
     1698#: includes/register-kinds.php:623
     1699msgid "Summary"
     1700msgstr ""
     1701
     1702#: includes/register-kinds.php:81
     1703#: includes/register-kinds.php:150
     1704#: includes/register-kinds.php:219
     1705#: includes/register-kinds.php:288
     1706#: includes/register-kinds.php:357
     1707#: includes/register-kinds.php:493
     1708msgid "Publication/Website"
     1709msgstr ""
     1710
     1711#: includes/register-kinds.php:85
     1712#: includes/register-kinds.php:154
     1713#: includes/register-kinds.php:223
     1714#: includes/register-kinds.php:292
     1715#: includes/register-kinds.php:361
     1716msgid "Publish Date"
     1717msgstr ""
     1718
     1719#: includes/register-kinds.php:89
     1720#: includes/register-kinds.php:158
     1721#: includes/register-kinds.php:227
     1722#: includes/register-kinds.php:296
     1723#: includes/register-kinds.php:365
     1724msgid "Updated Time"
     1725msgstr ""
     1726
     1727#: includes/register-kinds.php:93
     1728#: includes/register-kinds.php:162
     1729#: includes/register-kinds.php:231
     1730#: includes/register-kinds.php:300
     1731#: includes/register-kinds.php:369
     1732#: includes/register-kinds.php:445
     1733#: includes/register-kinds.php:505
     1734#: includes/register-kinds.php:570
     1735#: includes/register-kinds.php:635
     1736msgid "Tags"
     1737msgstr ""
     1738
     1739#: includes/register-kinds.php:100
     1740#: includes/register-kinds.php:169
     1741#: includes/register-kinds.php:238
     1742#: includes/register-kinds.php:307
     1743#: includes/register-kinds.php:376
     1744#: includes/register-kinds.php:512
     1745msgid "Featured Media"
     1746msgstr ""
     1747
     1748#: includes/register-kinds.php:109
     1749msgid "a reply to content typically on another site"
     1750msgstr ""
     1751
     1752#: includes/register-kinds.php:120
     1753#: includes/register-kinds.php:127
     1754msgid "Repost"
     1755msgstr ""
     1756
     1757#: includes/register-kinds.php:121
     1758msgid "Reposts"
     1759msgstr ""
     1760
    16001761#: includes/register-kinds.php:122
     1762msgid "Reposted"
     1763msgstr ""
     1764
     1765#: includes/register-kinds.php:178
     1766msgid "a complete reposting of content from another site"
     1767msgstr ""
     1768
     1769#: includes/register-kinds.php:189
     1770#: includes/register-kinds.php:196
     1771msgid "Like"
     1772msgstr ""
     1773
     1774#: includes/register-kinds.php:190
     1775msgid "Likes"
     1776msgstr ""
     1777
    16011778#: includes/register-kinds.php:191
     1779msgid "Liked"
     1780msgstr ""
     1781
     1782#: includes/register-kinds.php:247
     1783msgid "a way to pay compliments to the original post/poster of external content"
     1784msgstr ""
     1785
     1786#: includes/register-kinds.php:258
     1787msgid "Favorite"
     1788msgstr ""
     1789
     1790#: includes/register-kinds.php:259
     1791msgid "Favorites"
     1792msgstr ""
     1793
    16021794#: includes/register-kinds.php:260
     1795#: includes/register-kinds.php:265
     1796msgid "Favorited"
     1797msgstr ""
     1798
     1799#: includes/register-kinds.php:316
     1800msgid "special to the author"
     1801msgstr ""
     1802
     1803#: includes/register-kinds.php:327
     1804#: includes/register-kinds.php:334
     1805msgid "Bookmark"
     1806msgstr ""
     1807
     1808#: includes/register-kinds.php:328
     1809msgid "Bookmarks"
     1810msgstr ""
     1811
    16031812#: includes/register-kinds.php:329
    1604 #: includes/register-kinds.php:472
     1813msgid "Bookmarked"
     1814msgstr ""
     1815
     1816#: includes/register-kinds.php:385
     1817msgid "storing a link/bookmark for personal use or sharing with others"
     1818msgstr ""
     1819
     1820#: includes/register-kinds.php:396
     1821msgid "Photo"
     1822msgstr ""
     1823
     1824#: includes/register-kinds.php:397
     1825msgid "Photos"
     1826msgstr ""
     1827
     1828#: includes/register-kinds.php:401
     1829msgid "a post with an embedded image/photo as its primary focus"
     1830msgstr ""
     1831
     1832#: includes/register-kinds.php:412
     1833msgid "Video"
     1834msgstr ""
     1835
     1836#: includes/register-kinds.php:413
     1837msgid "Videos"
     1838msgstr ""
     1839
     1840#: includes/register-kinds.php:417
     1841msgid "a post with an embedded video as its primary focus"
     1842msgstr ""
     1843
     1844#: includes/register-kinds.php:428
     1845msgid "Audio"
     1846msgstr ""
     1847
     1848#: includes/register-kinds.php:429
     1849msgid "Audios"
     1850msgstr ""
     1851
     1852#: includes/register-kinds.php:433
     1853msgid "a post with an embedded audio file as its primary focus"
     1854msgstr ""
     1855
     1856#: includes/register-kinds.php:444
     1857msgid "Tag"
     1858msgstr ""
     1859
     1860#: includes/register-kinds.php:446
     1861msgid "Tagged"
     1862msgstr ""
     1863
     1864#: includes/register-kinds.php:449
     1865msgid "allows you to tag a post as being of a specific category or tag, or for person tagging"
     1866msgstr ""
     1867
     1868#: includes/register-kinds.php:461
     1869msgid "RSVPs"
     1870msgstr ""
     1871
     1872#: includes/register-kinds.php:462
     1873msgid "RSVPed"
     1874msgstr ""
     1875
     1876#: includes/register-kinds.php:477
     1877msgid "Event Details"
     1878msgstr ""
     1879
     1880#: includes/register-kinds.php:497
     1881msgid "Start Date"
     1882msgstr ""
     1883
     1884#: includes/register-kinds.php:501
     1885msgid "End Date"
     1886msgstr ""
     1887
     1888#: includes/register-kinds.php:521
     1889msgid "a specific type of reply regarding attendance of an event"
     1890msgstr ""
     1891
     1892#: includes/register-kinds.php:532
     1893msgid "Listen"
     1894msgstr ""
     1895
     1896#: includes/register-kinds.php:533
     1897msgid "Listens"
     1898msgstr ""
     1899
    16051900#: includes/register-kinds.php:534
     1901msgid "Listened"
     1902msgstr ""
     1903
     1904#: includes/register-kinds.php:539
     1905msgid "Listen To"
     1906msgstr ""
     1907
     1908#: includes/register-kinds.php:562
     1909msgid "Album"
     1910msgstr ""
     1911
     1912#: includes/register-kinds.php:566
     1913#: includes/register-kinds.php:631
     1914msgid "Release Date"
     1915msgstr ""
     1916
     1917#: includes/register-kinds.php:577
     1918msgid "Album Art"
     1919msgstr ""
     1920
     1921#: includes/register-kinds.php:586
     1922msgid "listening to audio; sometimes called a scrobble"
     1923msgstr ""
     1924
     1925#: includes/register-kinds.php:597
     1926msgid "Watch"
     1927msgstr ""
     1928
     1929#: includes/register-kinds.php:598
     1930msgid "Watches"
     1931msgstr ""
     1932
    16061933#: includes/register-kinds.php:599
     1934msgid "Watched"
     1935msgstr ""
     1936
     1937#: includes/register-kinds.php:604
     1938msgid "Watching To"
     1939msgstr ""
     1940
     1941#: includes/register-kinds.php:627
     1942msgid "Series"
     1943msgstr ""
     1944
     1945#: includes/register-kinds.php:642
     1946msgid "Poster"
     1947msgstr ""
     1948
     1949#: includes/register-kinds.php:651
     1950msgid "watching a movie, television show, online video, play or other visual-based event"
     1951msgstr ""
     1952
     1953#: includes/register-kinds.php:662
     1954msgid "Checkin"
     1955msgstr ""
     1956
     1957#: includes/register-kinds.php:663
     1958msgid "Checkins"
     1959msgstr ""
     1960
    16071961#: includes/register-kinds.php:664
    1608 #: templates/reply-metabox.php:42
    1609 msgid "Name"
    1610 msgstr ""
    1611 
    1612 #: includes/register-kinds.php:57
    1613 #: includes/register-kinds.php:126
    1614 #: includes/register-kinds.php:195
    1615 #: includes/register-kinds.php:264
    1616 #: includes/register-kinds.php:333
    1617 #: includes/register-kinds.php:476
    1618 #: includes/register-kinds.php:538
    1619 #: includes/register-kinds.php:603
    1620 #: includes/register-kinds.php:668
    1621 #: lib/parse-this/includes/class-rest-parse-this.php:49
    1622 #: templates/reply-metabox.php:36
    1623 msgid "URL"
    1624 msgstr ""
    1625 
    1626 #: includes/register-kinds.php:61
    1627 #: includes/register-kinds.php:130
    1628 #: includes/register-kinds.php:199
    1629 #: includes/register-kinds.php:268
    1630 #: includes/register-kinds.php:337
    1631 #: templates/reply-author.php:6
    1632 #: templates/reply-metabox.php:31
    1633 msgid "Author"
    1634 msgstr ""
    1635 
    1636 #: includes/register-kinds.php:68
    1637 #: includes/register-kinds.php:137
    1638 #: includes/register-kinds.php:206
    1639 #: includes/register-kinds.php:275
    1640 #: includes/register-kinds.php:344
    1641 #: includes/register-kinds.php:480
    1642 #: includes/register-kinds.php:549
    1643 #: includes/register-kinds.php:614
    1644 msgid "Summary"
    1645 msgstr ""
    1646 
    1647 #: includes/register-kinds.php:72
    1648 #: includes/register-kinds.php:141
    1649 #: includes/register-kinds.php:210
    1650 #: includes/register-kinds.php:279
    1651 #: includes/register-kinds.php:348
    1652 #: includes/register-kinds.php:484
    1653 msgid "Publication/Website"
    1654 msgstr ""
    1655 
    1656 #: includes/register-kinds.php:76
    1657 #: includes/register-kinds.php:145
    1658 #: includes/register-kinds.php:214
    1659 #: includes/register-kinds.php:283
    1660 #: includes/register-kinds.php:352
    1661 msgid "Publish Date"
    1662 msgstr ""
    1663 
    1664 #: includes/register-kinds.php:80
    1665 #: includes/register-kinds.php:149
    1666 #: includes/register-kinds.php:218
    1667 #: includes/register-kinds.php:287
    1668 #: includes/register-kinds.php:356
    1669 msgid "Updated Time"
    1670 msgstr ""
    1671 
    1672 #: includes/register-kinds.php:84
    1673 #: includes/register-kinds.php:153
    1674 #: includes/register-kinds.php:222
    1675 #: includes/register-kinds.php:291
    1676 #: includes/register-kinds.php:360
    1677 #: includes/register-kinds.php:436
    1678 #: includes/register-kinds.php:496
    1679 #: includes/register-kinds.php:561
    1680 #: includes/register-kinds.php:626
    1681 msgid "Tags"
    1682 msgstr ""
    1683 
    1684 #: includes/register-kinds.php:91
    1685 #: includes/register-kinds.php:160
    1686 #: includes/register-kinds.php:229
    1687 #: includes/register-kinds.php:298
    1688 #: includes/register-kinds.php:367
    1689 #: includes/register-kinds.php:503
    1690 msgid "Featured Media"
    1691 msgstr ""
    1692 
    1693 #: includes/register-kinds.php:100
    1694 msgid "a reply to content typically on another site"
    1695 msgstr ""
    1696 
    1697 #: includes/register-kinds.php:111
    1698 #: includes/register-kinds.php:118
    1699 msgid "Repost"
    1700 msgstr ""
    1701 
    1702 #: includes/register-kinds.php:112
    1703 msgid "Reposts"
    1704 msgstr ""
    1705 
    1706 #: includes/register-kinds.php:113
    1707 msgid "Reposted"
    1708 msgstr ""
    1709 
    1710 #: includes/register-kinds.php:169
    1711 msgid "a complete reposting of content from another site"
    1712 msgstr ""
    1713 
    1714 #: includes/register-kinds.php:180
    1715 #: includes/register-kinds.php:187
    1716 msgid "Like"
    1717 msgstr ""
    1718 
    1719 #: includes/register-kinds.php:181
    1720 msgid "Likes"
    1721 msgstr ""
    1722 
    1723 #: includes/register-kinds.php:182
    1724 msgid "Liked"
    1725 msgstr ""
    1726 
    1727 #: includes/register-kinds.php:238
    1728 msgid "a way to pay compliments to the original post/poster of external content"
    1729 msgstr ""
    1730 
    1731 #: includes/register-kinds.php:249
    1732 msgid "Favorite"
    1733 msgstr ""
    1734 
    1735 #: includes/register-kinds.php:250
    1736 msgid "Favorites"
    1737 msgstr ""
    1738 
    1739 #: includes/register-kinds.php:251
    1740 #: includes/register-kinds.php:256
    1741 msgid "Favorited"
    1742 msgstr ""
    1743 
    1744 #: includes/register-kinds.php:307
    1745 msgid "special to the author"
    1746 msgstr ""
    1747 
    1748 #: includes/register-kinds.php:318
    1749 #: includes/register-kinds.php:325
    1750 msgid "Bookmark"
    1751 msgstr ""
    1752 
    1753 #: includes/register-kinds.php:319
    1754 msgid "Bookmarks"
    1755 msgstr ""
    1756 
    1757 #: includes/register-kinds.php:320
    1758 msgid "Bookmarked"
    1759 msgstr ""
    1760 
    1761 #: includes/register-kinds.php:376
    1762 msgid "storing a link/bookmark for personal use or sharing with others"
    1763 msgstr ""
    1764 
    1765 #: includes/register-kinds.php:387
    1766 msgid "Photo"
    1767 msgstr ""
    1768 
    1769 #: includes/register-kinds.php:388
    1770 msgid "Photos"
    1771 msgstr ""
    1772 
    1773 #: includes/register-kinds.php:392
    1774 msgid "a post with an embedded image/photo as its primary focus"
    1775 msgstr ""
    1776 
    1777 #: includes/register-kinds.php:403
    1778 msgid "Video"
    1779 msgstr ""
    1780 
    1781 #: includes/register-kinds.php:404
    1782 msgid "Videos"
    1783 msgstr ""
    1784 
    1785 #: includes/register-kinds.php:408
    1786 msgid "a post with an embedded video as its primary focus"
    1787 msgstr ""
    1788 
    1789 #: includes/register-kinds.php:419
    1790 msgid "Audio"
    1791 msgstr ""
    1792 
    1793 #: includes/register-kinds.php:420
    1794 msgid "Audios"
    1795 msgstr ""
    1796 
    1797 #: includes/register-kinds.php:424
    1798 msgid "a post with an embedded audio file as its primary focus"
    1799 msgstr ""
    1800 
    1801 #: includes/register-kinds.php:435
    1802 msgid "Tag"
    1803 msgstr ""
    1804 
    1805 #: includes/register-kinds.php:437
    1806 msgid "Tagged"
    1807 msgstr ""
    1808 
    1809 #: includes/register-kinds.php:440
    1810 msgid "allows you to tag a post as being of a specific category or tag, or for person tagging"
    1811 msgstr ""
    1812 
    1813 #: includes/register-kinds.php:452
    1814 msgid "RSVPs"
    1815 msgstr ""
    1816 
    1817 #: includes/register-kinds.php:453
    1818 msgid "RSVPed"
    1819 msgstr ""
    1820 
    1821 #: includes/register-kinds.php:468
    1822 msgid "Event Details"
    1823 msgstr ""
    1824 
    1825 #: includes/register-kinds.php:488
    1826 msgid "Start Date"
    1827 msgstr ""
    1828 
    1829 #: includes/register-kinds.php:492
    1830 msgid "End Date"
    1831 msgstr ""
    1832 
    1833 #: includes/register-kinds.php:512
    1834 msgid "a specific type of reply regarding attendance of an event"
    1835 msgstr ""
    1836 
    1837 #: includes/register-kinds.php:523
    1838 msgid "Listen"
    1839 msgstr ""
    1840 
    1841 #: includes/register-kinds.php:524
    1842 msgid "Listens"
    1843 msgstr ""
    1844 
    1845 #: includes/register-kinds.php:525
    1846 msgid "Listened"
    1847 msgstr ""
    1848 
    1849 #: includes/register-kinds.php:530
    1850 msgid "Listen To"
    1851 msgstr ""
    1852 
    1853 #: includes/register-kinds.php:553
    1854 msgid "Album"
    1855 msgstr ""
    1856 
    1857 #: includes/register-kinds.php:557
    1858 #: includes/register-kinds.php:622
    1859 msgid "Release Date"
    1860 msgstr ""
    1861 
    1862 #: includes/register-kinds.php:568
    1863 msgid "Album Art"
    1864 msgstr ""
    1865 
    1866 #: includes/register-kinds.php:577
    1867 msgid "listening to audio; sometimes called a scrobble"
    1868 msgstr ""
    1869 
    1870 #: includes/register-kinds.php:588
    1871 msgid "Watch"
    1872 msgstr ""
    1873 
    1874 #: includes/register-kinds.php:589
    1875 msgid "Watches"
    1876 msgstr ""
    1877 
    1878 #: includes/register-kinds.php:590
    1879 msgid "Watched"
    1880 msgstr ""
    1881 
    1882 #: includes/register-kinds.php:595
    1883 msgid "Watching To"
    1884 msgstr ""
    1885 
    1886 #: includes/register-kinds.php:618
    1887 msgid "Series"
    1888 msgstr ""
    1889 
    1890 #: includes/register-kinds.php:633
    1891 msgid "Poster"
    1892 msgstr ""
    1893 
    1894 #: includes/register-kinds.php:642
    1895 msgid "watching a movie, television show, online video, play or other visual-based event"
    1896 msgstr ""
    1897 
    1898 #: includes/register-kinds.php:653
    1899 msgid "Checkin"
    1900 msgstr ""
    1901 
    1902 #: includes/register-kinds.php:654
    1903 msgid "Checkins"
    1904 msgstr ""
    1905 
    1906 #: includes/register-kinds.php:655
    19071962msgid "Checked into"
    19081963msgstr ""
    19091964
    1910 #: includes/register-kinds.php:660
     1965#: includes/register-kinds.php:669
    19111966msgid "Checked In"
    19121967msgstr ""
    19131968
    1914 #: includes/register-kinds.php:672
     1969#: includes/register-kinds.php:681
    19151970msgid "Latitude"
    19161971msgstr ""
    19171972
    1918 #: includes/register-kinds.php:680
     1973#: includes/register-kinds.php:689
    19191974msgid "Longitude"
    19201975msgstr ""
    19211976
    1922 #: includes/register-kinds.php:688
     1977#: includes/register-kinds.php:697
    19231978msgid "Street Address"
    19241979msgstr ""
    19251980
    1926 #: includes/register-kinds.php:692
     1981#: includes/register-kinds.php:701
    19271982msgid "Locality"
    19281983msgstr ""
    19291984
    1930 #: includes/register-kinds.php:696
     1985#: includes/register-kinds.php:705
    19311986msgid "Region"
    19321987msgstr ""
    19331988
    1934 #: includes/register-kinds.php:700
     1989#: includes/register-kinds.php:709
    19351990msgid "Country Name"
    19361991msgstr ""
    19371992
    1938 #: includes/register-kinds.php:704
     1993#: includes/register-kinds.php:713
    19391994msgid "Postal Code"
    19401995msgstr ""
    19411996
    1942 #: includes/register-kinds.php:710
     1997#: includes/register-kinds.php:719
    19431998msgid "identifying you are at a particular geographic location"
    19441999msgstr ""
    19452000
    1946 #: includes/register-kinds.php:721
     2001#: includes/register-kinds.php:730
    19472002msgid "Wish"
    19482003msgstr ""
    19492004
    1950 #: includes/register-kinds.php:722
     2005#: includes/register-kinds.php:731
    19512006msgid "Wishes"
    19522007msgstr ""
    19532008
    1954 #: includes/register-kinds.php:723
     2009#: includes/register-kinds.php:732
    19552010msgid "Wished"
    19562011msgstr ""
    19572012
    1958 #: includes/register-kinds.php:726
     2013#: includes/register-kinds.php:735
    19592014msgid "a post indicating a desire/wish. The archive of which would be a wishlist, such as a gift registry or similar"
    19602015msgstr ""
    19612016
    1962 #: includes/register-kinds.php:737
     2017#: includes/register-kinds.php:746
    19632018msgid "Play"
    19642019msgstr ""
    19652020
    1966 #: includes/register-kinds.php:738
     2021#: includes/register-kinds.php:747
    19672022msgid "Playing"
    19682023msgstr ""
    19692024
    1970 #: includes/register-kinds.php:739
     2025#: includes/register-kinds.php:748
    19712026msgid "Played"
    19722027msgstr ""
    19732028
    1974 #: includes/register-kinds.php:742
     2029#: includes/register-kinds.php:751
    19752030msgid "playing a game"
    19762031msgstr ""
    19772032
    1978 #: includes/register-kinds.php:753
    1979 #: includes/register-kinds.php:754
     2033#: includes/register-kinds.php:762
     2034#: includes/register-kinds.php:763
    19802035msgid "Weather"
    19812036msgstr ""
    19822037
    1983 #: includes/register-kinds.php:758
     2038#: includes/register-kinds.php:767
    19842039msgid "current weather conditions"
    19852040msgstr ""
    19862041
    1987 #: includes/register-kinds.php:769
    1988 #: includes/register-kinds.php:770
     2042#: includes/register-kinds.php:778
     2043#: includes/register-kinds.php:779
    19892044msgid "Exercise"
    19902045msgstr ""
    19912046
    1992 #: includes/register-kinds.php:771
     2047#: includes/register-kinds.php:780
    19932048msgid "Exercised"
    19942049msgstr ""
    19952050
    1996 #: includes/register-kinds.php:774
     2051#: includes/register-kinds.php:783
    19972052msgid "some form of physical activity or workout (examples: walk, run, cycle, hike, yoga, etc.)"
    19982053msgstr ""
    19992054
    2000 #: includes/register-kinds.php:785
     2055#: includes/register-kinds.php:794
    20012056msgid "Trip"
    20022057msgstr ""
    20032058
    2004 #: includes/register-kinds.php:786
     2059#: includes/register-kinds.php:795
    20052060msgid "Trips"
    20062061msgstr ""
    20072062
    2008 #: includes/register-kinds.php:787
    2009 #: includes/register-kinds.php:803
     2063#: includes/register-kinds.php:796
     2064#: includes/register-kinds.php:812
    20102065msgid "Travelled"
    20112066msgstr ""
    20122067
    2013 #: includes/register-kinds.php:790
     2068#: includes/register-kinds.php:799
    20142069msgid "represents a geographic journey"
    20152070msgstr ""
    20162071
    2017 #: includes/register-kinds.php:801
     2072#: includes/register-kinds.php:810
    20182073msgid "Itinerary"
    20192074msgstr ""
    20202075
    2021 #: includes/register-kinds.php:802
     2076#: includes/register-kinds.php:811
    20222077msgid "Itineraries"
    20232078msgstr ""
    20242079
    2025 #: includes/register-kinds.php:806
     2080#: includes/register-kinds.php:815
    20262081msgid "parts of a scheduled trip including transit by car, plane, train, etc."
    20272082msgstr ""
    20282083
    2029 #: includes/register-kinds.php:818
    2030 #: includes/register-kinds.php:819
     2084#: includes/register-kinds.php:827
     2085#: includes/register-kinds.php:828
    20312086msgid "Eat"
    20322087msgstr ""
    20332088
    2034 #: includes/register-kinds.php:820
     2089#: includes/register-kinds.php:829
    20352090msgid "Ate"
    20362091msgstr ""
    20372092
    2038 #: includes/register-kinds.php:823
     2093#: includes/register-kinds.php:832
    20392094msgid "what you are eating, perhaps for a food diary"
    20402095msgstr ""
    20412096
    2042 #: includes/register-kinds.php:835
     2097#: includes/register-kinds.php:844
    20432098msgid "Drink"
    20442099msgstr ""
    20452100
    2046 #: includes/register-kinds.php:836
     2101#: includes/register-kinds.php:845
    20472102msgid "Drinks"
    20482103msgstr ""
    20492104
    2050 #: includes/register-kinds.php:837
     2105#: includes/register-kinds.php:846
    20512106msgid "Drank"
    20522107msgstr ""
    20532108
    2054 #: includes/register-kinds.php:840
     2109#: includes/register-kinds.php:849
    20552110msgid "what you are drinking, perhaps for a food dairy"
    20562111msgstr ""
    20572112
    2058 #: includes/register-kinds.php:852
     2113#: includes/register-kinds.php:861
    20592114msgid "Follow"
    20602115msgstr ""
    20612116
    2062 #: includes/register-kinds.php:853
     2117#: includes/register-kinds.php:862
    20632118msgid "Follows"
    20642119msgstr ""
    20652120
    2066 #: includes/register-kinds.php:854
     2121#: includes/register-kinds.php:863
    20672122msgid "Followed"
    20682123msgstr ""
    20692124
    2070 #: includes/register-kinds.php:857
     2125#: includes/register-kinds.php:866
    20712126msgid "indicating you are now following or subscribing to another person`s activities online"
    20722127msgstr ""
    20732128
    2074 #: includes/register-kinds.php:869
     2129#: includes/register-kinds.php:878
    20752130msgid "Jam"
    20762131msgstr ""
    20772132
    2078 #: includes/register-kinds.php:870
     2133#: includes/register-kinds.php:879
    20792134msgid "Jams"
    20802135msgstr ""
    20812136
    2082 #: includes/register-kinds.php:871
     2137#: includes/register-kinds.php:880
    20832138msgid "Listened to"
    20842139msgstr ""
    20852140
    2086 #: includes/register-kinds.php:874
     2141#: includes/register-kinds.php:883
    20872142msgid "a particularly personally meaningful song (a listen with added emphasis)"
    20882143msgstr ""
    20892144
    2090 #: includes/register-kinds.php:886
    2091 #: includes/register-kinds.php:888
     2145#: includes/register-kinds.php:895
     2146#: includes/register-kinds.php:897
    20922147msgid "Read"
    20932148msgstr ""
    20942149
    2095 #: includes/register-kinds.php:887
     2150#: includes/register-kinds.php:896
    20962151msgid "Reads"
    20972152msgstr ""
    20982153
    2099 #: includes/register-kinds.php:891
     2154#: includes/register-kinds.php:900
    21002155msgid "reading a book, magazine, newspaper, other physical document, or online post"
    21012156msgstr ""
    21022157
    2103 #: includes/register-kinds.php:903
     2158#: includes/register-kinds.php:912
    21042159msgid "Quote"
    21052160msgstr ""
    21062161
    2107 #: includes/register-kinds.php:904
     2162#: includes/register-kinds.php:913
    21082163msgid "Quotes"
    21092164msgstr ""
    21102165
    2111 #: includes/register-kinds.php:905
     2166#: includes/register-kinds.php:914
    21122167msgid "Quoted"
    21132168msgstr ""
    21142169
    2115 #: includes/register-kinds.php:908
     2170#: includes/register-kinds.php:917
    21162171msgid "quoted content"
    21172172msgstr ""
    21182173
    2119 #: includes/register-kinds.php:920
     2174#: includes/register-kinds.php:929
    21202175msgid "Mood"
    21212176msgstr ""
    21222177
    2123 #: includes/register-kinds.php:921
     2178#: includes/register-kinds.php:930
    21242179msgid "Moods"
    21252180msgstr ""
    21262181
    2127 #: includes/register-kinds.php:922
     2182#: includes/register-kinds.php:931
    21282183msgid "Felt"
    21292184msgstr ""
    21302185
    2131 #: includes/register-kinds.php:925
     2186#: includes/register-kinds.php:934
    21322187msgid "how you are feeling (example: happy, sad, indifferent, etc.)"
    21332188msgstr ""
    21342189
    2135 #: includes/register-kinds.php:937
     2190#: includes/register-kinds.php:946
    21362191msgid "Recipe"
    21372192msgstr ""
    21382193
    2139 #: includes/register-kinds.php:938
     2194#: includes/register-kinds.php:947
    21402195msgid "Recipes"
    21412196msgstr ""
    21422197
    2143 #: includes/register-kinds.php:939
     2198#: includes/register-kinds.php:948
    21442199msgid "Cooked"
    21452200msgstr ""
    21462201
    2147 #: includes/register-kinds.php:942
     2202#: includes/register-kinds.php:951
    21482203msgid "list of ingredients and directions for making food or drink"
    21492204msgstr ""
    21502205
    2151 #: includes/register-kinds.php:954
     2206#: includes/register-kinds.php:963
    21522207msgid "Issue"
    21532208msgstr ""
    21542209
    2155 #: includes/register-kinds.php:955
     2210#: includes/register-kinds.php:964
    21562211msgid "Issues"
    21572212msgstr ""
    21582213
    2159 #: includes/register-kinds.php:956
     2214#: includes/register-kinds.php:965
    21602215msgid "Filed an Issue"
    21612216msgstr ""
    21622217
    2163 #: includes/register-kinds.php:959
     2218#: includes/register-kinds.php:968
    21642219msgid "Issue is a special kind of article post that is a reply to typically some source code, though potentially anything at a source control repository."
    21652220msgstr ""
    21662221
    2167 #: includes/register-kinds.php:971
     2222#: includes/register-kinds.php:980
    21682223msgid "Question"
    21692224msgstr ""
    21702225
    2171 #: includes/register-kinds.php:972
     2226#: includes/register-kinds.php:981
    21722227msgid "Questions"
    21732228msgstr ""
    21742229
    2175 #: includes/register-kinds.php:973
     2230#: includes/register-kinds.php:982
    21762231msgid "Asked a question"
    21772232msgstr ""
    21782233
    2179 #: includes/register-kinds.php:976
     2234#: includes/register-kinds.php:985
    21802235msgid "Question is a post type for soliciting answer replies, which are then typically up/down voted by others and then displayed underneath the question post ordered by highest positive vote count rather than time ordered."
    21812236msgstr ""
    21822237
    2183 #: includes/register-kinds.php:988
     2238#: includes/register-kinds.php:997
    21842239msgid "Sleep"
    21852240msgstr ""
    21862241
    2187 #: includes/register-kinds.php:989
     2242#: includes/register-kinds.php:998
    21882243msgid "Sleeps"
    21892244msgstr ""
    21902245
    2191 #: includes/register-kinds.php:990
     2246#: includes/register-kinds.php:999
    21922247msgid "Slept"
    21932248msgstr ""
    21942249
    2195 #: includes/register-kinds.php:993
     2250#: includes/register-kinds.php:1002
    21962251msgid "Sleep is a passive metrics post type that indicates how much time (and often a graph of how deeply) a person has slept."
    21972252msgstr ""
    21982253
    2199 #: includes/register-kinds.php:1005
     2254#: includes/register-kinds.php:1014
    22002255msgid "Event"
    22012256msgstr ""
    22022257
    2203 #: includes/register-kinds.php:1006
     2258#: includes/register-kinds.php:1015
    22042259msgid "Events"
    22052260msgstr ""
    22062261
    2207 #: includes/register-kinds.php:1007
     2262#: includes/register-kinds.php:1016
    22082263msgid "Planned"
    22092264msgstr ""
    22102265
    2211 #: includes/register-kinds.php:1010
     2266#: includes/register-kinds.php:1019
    22122267msgid "An event is a type of post that in addition to a post name (event title) has a start datetime (likely end datetime), and a location."
    22132268msgstr ""
    22142269
    2215 #: includes/register-kinds.php:1021
     2270#: includes/register-kinds.php:1030
    22162271msgid "Review"
    22172272msgstr ""
    22182273
    2219 #: includes/register-kinds.php:1022
     2274#: includes/register-kinds.php:1031
    22202275msgid "Reviews"
    22212276msgstr ""
    22222277
    2223 #: includes/register-kinds.php:1023
     2278#: includes/register-kinds.php:1032
    22242279msgid "Reviewed"
    22252280msgstr ""
    22262281
    2227 #: includes/register-kinds.php:1026
     2282#: includes/register-kinds.php:1035
    22282283msgid "A review is a post evaluating a product or service, usually involving a written description, sometimes with summary numerical evaluations, also known as just a rating."
    22292284msgstr ""
    22302285
    2231 #: includes/register-kinds.php:1038
     2286#: includes/register-kinds.php:1047
    22322287msgid "Acquisition"
    22332288msgstr ""
    22342289
    2235 #: includes/register-kinds.php:1039
     2290#: includes/register-kinds.php:1048
    22362291msgid "Acquisitions"
    22372292msgstr ""
    22382293
    2239 #: includes/register-kinds.php:1040
     2294#: includes/register-kinds.php:1049
    22402295msgid "Acquired"
    22412296msgstr ""
    22422297
    2243 #: includes/register-kinds.php:1043
     2298#: includes/register-kinds.php:1052
    22442299msgid "Purchases, gifts, found things, or objects donated"
    22452300msgstr ""
    22462301
    2247 #: includes/register-kinds.php:1054
     2302#: includes/register-kinds.php:1063
    22482303msgid "Craft"
    22492304msgstr ""
    22502305
    2251 #: includes/register-kinds.php:1055
     2306#: includes/register-kinds.php:1064
    22522307msgid "Crafts"
    22532308msgstr ""
    22542309
    2255 #: includes/register-kinds.php:1056
     2310#: includes/register-kinds.php:1065
    22562311msgid "Crafted"
    22572312msgstr ""
    22582313
    2259 #: includes/register-kinds.php:1059
     2314#: includes/register-kinds.php:1068
    22602315msgid "Activities like knitting, crocheting, cross stitch, wood working, restoration, 3d printing...the activity of building something."
    22612316msgstr ""
    22622317
    2263 #: indieweb-post-kinds.php:65
     2318#: indieweb-post-kinds.php:87
    22642319msgid "Parse This is not installed. Please advise the developer"
    22652320msgstr ""
    22662321
    2267 #: indieweb-post-kinds.php:98
     2322#. translators: %s: Name of the plugin, or path of the file, that loaded the older copy of Parse This.
     2323#: indieweb-post-kinds.php:109
     2324#, php-format
     2325msgid "Post Kinds: %s is loading a copy of Parse This older than version 2.0. Post Kinds will keep working, but URL lookups in the post editor use that older copy. Updating that plugin, or installing the Parse This plugin (version 2.0 or later), resolves this."
     2326msgstr ""
     2327
     2328#: indieweb-post-kinds.php:209
    22682329msgid "Classic Editor or Disable Gutenberg Plugin is not active. The Post Kinds plugin will not function correctly at this time without using the Classic Editor."
    22692330msgstr ""
    22702331
    2271 #: indieweb-post-kinds.php:211
     2332#: indieweb-post-kinds.php:353
    22722333msgid "Settings"
    22732334msgstr ""
    22742335
    2275 #: indieweb-post-kinds.php:233
     2336#: indieweb-post-kinds.php:384
    22762337msgid "For responses to URLs, such as responding to a post or article, this site allows the storage of data around the post/article in order to generate a rich citation. Items such as author name and image, summary of the text, embed provided by third-party site, etc may be stored and are solely to provide this context. We will remove any of this on request."
    22772338msgstr ""
    22782339
    2279 #: lib/parse-this/includes/class-parse-this-discovery.php:57
    2280 #: lib/parse-this/includes/class-parse-this-opml.php:17
    2281 #: lib/parse-this/includes/class-parse-this.php:231
    2282 #: lib/parse-this/includes/class-parse-this.php:265
    2283 msgid "A valid URL was not provided."
    2284 msgstr ""
    2285 
    2286 #: lib/parse-this/includes/class-rest-parse-this.php:19
    2287 #: lib/parse-this/includes/class-rest-parse-this.php:20
    2288 msgid "Parse This"
    2289 msgstr ""
    2290 
    2291 #: lib/parse-this/includes/class-rest-parse-this.php:35
    2292 msgid "Parse This Debugger"
    2293 msgstr ""
    2294 
    2295 #: lib/parse-this/includes/class-rest-parse-this.php:55
    2296 msgid "MF2"
    2297 msgstr ""
    2298 
    2299 #: lib/parse-this/includes/class-rest-parse-this.php:63
    2300 msgid "Feed Discovery"
    2301 msgstr ""
    2302 
    2303 #: lib/parse-this/includes/class-rest-parse-this.php:71
    2304 msgid "References"
    2305 msgstr ""
    2306 
    2307 #: lib/parse-this/includes/class-rest-parse-this.php:79
    2308 msgid "Clean up Location"
    2309 msgstr ""
    2310 
    2311 #: lib/parse-this/includes/class-rest-parse-this.php:87
    2312 msgid "Return Type"
    2313 msgstr ""
    2314 
    2315 #: lib/parse-this/includes/class-rest-parse-this.php:91
    2316 msgid "Single"
    2317 msgstr ""
    2318 
    2319 #: lib/parse-this/includes/class-rest-parse-this.php:92
    2320 msgid "Feed"
    2321 msgstr ""
    2322 
    2323 #: lib/parse-this/includes/class-rest-parse-this.php:98
    2324 msgid "Follow Author Links"
    2325 msgstr ""
    2326 
    2327 #: lib/parse-this/includes/class-rest-parse-this.php:107
    2328 msgid "Parse"
    2329 msgstr ""
    2330 
    2331 #: templates/reply-author.php:2
     2340#: templates/reply-author.php:16
    23322341msgid "Information on the Author or Artist of the Piece"
    23332342msgstr ""
    23342343
    2335 #: templates/reply-author.php:3
     2344#: templates/reply-author.php:17
    23362345msgid "(Multiple Entries separated by semicolon)"
    23372346msgstr ""
    23382347
    2339 #: templates/reply-author.php:18
     2348#: templates/reply-author.php:32
    23402349msgid "Author Photo URL"
    23412350msgstr ""
    23422351
    2343 #: templates/reply-details.php:2
     2352#: templates/reply-details.php:17
    23442353msgid "Information on what you are responding to"
    23452354msgstr ""
    23462355
    2347 #: templates/reply-details.php:4
     2356#: templates/reply-details.php:19
    23482357msgid "Summary/Quote/Caption"
    23492358msgstr ""
    23502359
    2351 #: templates/reply-details.php:9
     2360#: templates/reply-details.php:24
    23522361msgid "Site Name/Publication/Album"
    23532362msgstr ""
    23542363
    2355 #: templates/reply-details.php:14
     2364#: templates/reply-details.php:29
    23562365msgid "Published/Released"
    23572366msgstr ""
    23582367
    2359 #: templates/reply-details.php:15
     2368#: templates/reply-details.php:30
    23602369msgid "Updated"
    23612370msgstr ""
    23622371
    2363 #: templates/reply-details.php:18
     2372#: templates/reply-details.php:33
    23642373msgid "Tags (semicolon separated)"
    23652374msgstr ""
    23662375
    2367 #: templates/reply-details.php:23
     2376#: templates/reply-details.php:38
    23682377msgid "Featured Image"
    23692378msgstr ""
    23702379
    2371 #: templates/reply-metabox.php:30
     2380#: templates/reply-metabox.php:44
    23722381msgid "Details"
    23732382msgstr ""
    23742383
    2375 #: templates/reply-metabox.php:33
     2384#: templates/reply-metabox.php:46
     2385msgid "Upload or Attach Media"
     2386msgstr ""
     2387
     2388#: templates/reply-metabox.php:47
    23762389msgid "Clear"
    23772390msgstr ""
    23782391
    2379 #: templates/reply-time.php:4
     2392#: templates/reply-time.php:18
    23802393msgid "Duration"
    23812394msgstr ""
    23822395
    2383 #: templates/reply-time.php:7
     2396#: templates/reply-time.php:21
    23842397msgid "Years"
    23852398msgstr ""
    23862399
    2387 #: templates/reply-time.php:11
     2400#: templates/reply-time.php:25
    23882401msgid "Months"
    23892402msgstr ""
    23902403
    2391 #: templates/reply-time.php:15
     2404#: templates/reply-time.php:29
    23922405msgid "Days"
    23932406msgstr ""
    23942407
    2395 #: templates/reply-time.php:21
     2408#: templates/reply-time.php:35
    23962409msgid "Hours"
    23972410msgstr ""
    23982411
    2399 #: templates/reply-time.php:25
     2412#: templates/reply-time.php:39
    24002413msgid "Minutes"
    24012414msgstr ""
    24022415
    2403 #: templates/reply-time.php:29
     2416#: templates/reply-time.php:43
    24042417msgid "Seconds"
    24052418msgstr ""
    24062419
    2407 #: templates/reply-time.php:33
     2420#: templates/reply-time.php:47
    24082421msgid "Start Time and End Time will be Used to Calculate Duration if Duration Not Set."
    24092422msgstr ""
    24102423
    2411 #: templates/reply-time.php:35
     2424#: templates/reply-time.php:49
    24122425msgid "Start Time"
    24132426msgstr ""
    24142427
    2415 #: templates/reply-time.php:36
     2428#: templates/reply-time.php:50
    24162429msgid "End Time"
    24172430msgstr ""
    24182431
    2419 #: views/kind-bookmark.php:25
    2420 #: views/kind-favorite.php:24
    2421 #: views/kind-issue.php:24
    2422 #: views/kind-jam.php:28
    2423 #: views/kind-like.php:25
    2424 #: views/kind-listen.php:30
    2425 #: views/kind-read.php:28
    2426 #: views/kind-video.php:45
    2427 #: views/kind-watch.php:29
    2428 #: views/kind.php:35
     2432#: views/kind-bookmark.php:39
     2433#: views/kind-favorite.php:38
     2434#: views/kind-issue.php:38
     2435#: views/kind-jam.php:46
     2436#: views/kind-like.php:39
     2437#: views/kind-listen.php:48
     2438#: views/kind-read.php:42
     2439#: views/kind-video.php:63
     2440#: views/kind-watch.php:47
     2441#: views/kind.php:53
    24292442msgid "by"
    24302443msgstr ""
    24312444
    2432 #: views/kind-event.php:22
     2445#: views/kind-event.php:39
    24332446msgid "Start"
    24342447msgstr ""
    24352448
    2436 #: views/kind-event.php:25
     2449#: views/kind-event.php:42
    24372450msgid "End"
    24382451msgstr ""
    24392452
    2440 #: views/kind-itinerary.php:33
     2453#: views/kind-itinerary.php:56
    24412454msgid "Departs: "
    24422455msgstr ""
    24432456
    2444 #: views/kind-itinerary.php:38
     2457#: views/kind-itinerary.php:61
    24452458msgid "Arrives: "
    24462459msgstr ""
    24472460
    2448 #: views/kind-jam.php:31
    2449 #: views/kind-listen.php:33
    2450 #: views/kind-watch.php:32
     2461#: views/kind-jam.php:49
     2462#: views/kind-listen.php:51
     2463#: views/kind-watch.php:50
    24512464msgid " from "
    24522465msgstr ""
  • indieweb-post-kinds/trunk/lib/parse-this/includes/autoload.php

    r2332001 r3738687  
    11<?php
     2/**
     3 * Class autoloader for Parse This.
     4 *
     5 * @package Parse_This
     6 */
    27
    3 // autoloader for Parse This
     8defined( 'ABSPATH' ) || exit;
     9
     10/*
     11 * Maps classes in the ParseThis namespace to files in this directory, for
     12 * example ParseThis\MF2_Utils to class-mf2-utils.php.
     13 */
    414spl_autoload_register(
    515        function ( $class ) {
    6                 $base_dir = trailingslashit( __DIR__ );
    7                 $bases    = array( 'Parse_This' );
    8                 foreach ( $bases as $base ) {
    9                         if ( strncmp( $class, $base, strlen( $base ) ) === 0 ) {
    10                                 $filename = 'class-' . strtolower( str_replace( '_', '-', $class ) );
    11                                 $file     = $base_dir . $filename . '.php';
    12                                 if ( file_exists( $file ) ) {
    13                                         require $file;
    14                                 }
    15                         }
     16                $prefix = 'ParseThis\\';
     17                if ( 0 !== strncmp( $class, $prefix, strlen( $prefix ) ) ) {
     18                        return;
     19                }
     20                $name = substr( $class, strlen( $prefix ) );
     21                $file = __DIR__ . '/class-' . strtolower( str_replace( '_', '-', $name ) ) . '.php';
     22                if ( file_exists( $file ) ) {
     23                        require $file;
    1624                }
    1725        }
    1826);
     27
     28// Deprecated global names still used by other plugins.
     29require_once __DIR__ . '/aliases.php';
  • indieweb-post-kinds/trunk/lib/parse-this/includes/functions.php

    r2757240 r3738687  
    11<?php
    2 // Parse This Global Functions
    3 
    4 
    5 if ( ! function_exists( 'jf2_to_mf2' ) ) {
     2/**
     3 * Helper functions.
     4 *
     5 * All functions are in the ParseThis namespace. Each is wrapped in
     6 * function_exists() because several plugins bundle their own copy of Parse
     7 * This; the first copy loaded wins. Deprecated global names for some of them
     8 * are defined in aliases.php.
     9 *
     10 * @package Parse_This
     11 */
     12
     13namespace ParseThis;
     14
     15defined( 'ABSPATH' ) || exit;
     16
     17if ( ! function_exists( __NAMESPACE__ . '\\jf2_to_mf2' ) ) {
     18        /**
     19         * Converts jf2 into microformats2 JSON.
     20         *
     21         * The inverse of mf2_to_jf2(). Empty values and the _raw key are dropped.
     22         *
     23         * @since 1.0.0
     24         *
     25         * @link https://jf2.spec.indieweb.org/
     26         *
     27         * @param array|mixed $jf2 jf2 object, or array( 'items' => ... ) for a list.
     28         * @return array|mixed mf2 object with type and properties. Values that are not
     29         *                     jf2 objects are returned unchanged.
     30         */
    631        function jf2_to_mf2( $jf2 ) {
    732                if ( ! $jf2 || ! is_array( $jf2 ) ) {
    … …  
    1035                if ( 1 === count( $jf2 ) && array_key_exists( 'items', $jf2 ) ) {
    1136                        return array(
    12                                 'items' => array_map( 'jf2_to_mf2', $jf2['items'] ),
     37                                'items' => array_map( __NAMESPACE__ . '\\jf2_to_mf2', $jf2['items'] ),
    1338                        );
    1439                }
    … …  
    2449                }
    2550                if ( array_key_exists( 'children', $jf2 ) ) {
    26                         $mf2['children'] = array_map( 'jf2_to_mf2', $jf2['children'] );
     51                        $mf2['children'] = array_map( __NAMESPACE__ . '\\jf2_to_mf2', $jf2['children'] );
    2752                        unset( $jf2['children'] );
    2853                }
    … …  
    3156
    3257                foreach ( $jf2 as $key => $value ) {
    33                         // Exclude values
     58                        // Exclude values.
    3459                        if ( empty( $value ) || ( '_raw' === $key ) ) {
    3560                                continue;
    … …  
    3863                                $value = array( jf2_to_mf2( $value ) );
    3964                        } elseif ( wp_is_numeric_array( $value ) ) {
    40                                 $value = array_map( 'jf2_to_mf2', $value );
     65                                $value = array_map( __NAMESPACE__ . '\\jf2_to_mf2', $value );
    4166                        } elseif ( ! wp_is_numeric_array( $value ) ) {
    4267                                $value = array( $value );
    … …  
    4873}
    4974
    50 if ( ! function_exists( 'mf2_to_jf2' ) ) {
    51 
     75if ( ! function_exists( __NAMESPACE__ . '\\mf2_to_jf2' ) ) {
     76
     77        /**
     78         * Converts microformats2 JSON into jf2.
     79         *
     80         * Single-value property arrays are collapsed to the value, the h- prefix is
     81         * removed from types, and nested microformats are converted recursively.
     82         *
     83         * @since 1.0.0
     84         *
     85         * @link https://jf2.spec.indieweb.org/
     86         *
     87         * @param array|mixed $mf2 mf2 object, a parsed mf2 document with items, or a
     88         *                         list of mf2 objects.
     89         * @return array|mixed jf2. A list of one is returned as the single item.
     90         *                     Strings and objects are returned unchanged.
     91         */
    5292        function mf2_to_jf2( $mf2 ) {
    5393                if ( empty( $mf2 ) || is_string( $mf2 ) || is_object( $mf2 ) ) {
    … …  
    5797                $jf2 = array();
    5898
    59                 // If it is a numeric array, run this function through each item
     99                // If it is a numeric array, run this function through each item.
    60100                if ( wp_is_numeric_array( $mf2 ) ) {
    61                         $jf2 = array_map( 'mf2_to_jf2', $mf2 );
     101                        $jf2 = array_map( __NAMESPACE__ . '\\mf2_to_jf2', $mf2 );
    62102                        if ( 1 === count( $jf2 ) ) {
    63103                                return array_pop( $jf2 );
    … …  
    67107
    68108                if ( isset( $mf2['items'] ) ) {
    69                         $jf2['items'] = array_map( 'mf2_to_jf2', $mf2['items'] );
     109                        $jf2['items'] = array_map( __NAMESPACE__ . '\\mf2_to_jf2', $mf2['items'] );
    70110                }
    71111
    72112                if ( isset( $mf2['children'] ) ) {
    73                         $jf2['children'] = array_map( 'mf2_to_jf2', $mf2['children'] );
     113                        $jf2['children'] = array_map( __NAMESPACE__ . '\\mf2_to_jf2', $mf2['children'] );
    74114                }
    75115
    … …  
    82122                                if ( is_array( $value ) ) {
    83123                                        if ( wp_is_numeric_array( $value ) ) {
    84                                                 $value = array_map( 'mf2_to_jf2', $value );
     124                                                $value = array_map( __NAMESPACE__ . '\\mf2_to_jf2', $value );
    85125                                                if ( is_countable( $value ) && 1 === count( $value ) ) {
    86126                                                        $value = array_pop( $value );
    … …  
    98138
    99139
    100 if ( ! function_exists( 'jf2_location' ) ) {
    101         /*
    102          Flatten nested location properties.
    103         */
     140if ( ! function_exists( __NAMESPACE__ . '\\jf2_location' ) ) {
     141        /**
     142         * Flattens a nested jf2 location.
     143         *
     144         * The latitude, longitude and altitude are copied onto the object, and location
     145         * becomes the place's label or name (or is removed). Any check-in on the
     146         * object also receives the location's properties.
     147         *
     148         * @since 1.0.1
     149         *
     150         * @param array $data jf2 object.
     151         * @return array The updated object.
     152         */
    104153        function jf2_location( $data ) {
    105154                if ( ! array_key_exists( 'location', $data ) ) {
    106155                        return $data;
    107156                }
    108                 $location = $data['location'];
    109                 if ( is_string( $location ) ) {
     157                // jf2_references() may have moved the location or check-in to refs,
     158                // leaving its URL (as a one-item list) in its place.
     159                $from_refs = function ( $value ) use ( $data ) {
     160                        if ( is_array( $value ) && wp_is_numeric_array( $value ) && 1 === count( $value ) ) {
     161                                $value = $value[0];
     162                        }
     163                        if ( is_string( $value ) && isset( $data['refs'][ $value ] ) && is_array( $data['refs'][ $value ] ) ) {
     164                                return $data['refs'][ $value ];
     165                        }
     166                        return $value;
     167                };
     168                $location  = $from_refs( $data['location'] );
     169                if ( ! is_array( $location ) || wp_is_numeric_array( $location ) ) {
    110170                        return $data;
    111171                }
    … …  
    122182                        unset( $data['location'] );
    123183                }
     184                if ( isset( $data['checkin'] ) ) {
     185                        $data['checkin'] = $from_refs( $data['checkin'] );
     186                }
    124187                if ( array_key_exists( 'checkin', $data ) && is_array( $data['checkin'] ) ) {
    125188                        foreach ( $location as $key => $value ) {
    … …  
    134197
    135198
    136 if ( ! function_exists( 'jf2_references' ) ) {
    137         /*
    138          Turns nested properties into references per the jf2 spec
    139         */
     199if ( ! function_exists( __NAMESPACE__ . '\\jf2_author_to_card' ) ) {
     200        /**
     201         * Converts an author value into a jf2 card.
     202         *
     203         * Microsub requires author to always be a card, and jf2 consumers generally
     204         * expect one. A URL string becomes a card with that url, any other string a
     205         * card with that name, and an object without a type gets type card. A list of
     206         * authors is converted item by item; a list of one becomes that card.
     207         *
     208         * @since 2.0.0
     209         *
     210         * @param mixed $author Author: a string, an object or a list of either.
     211         * @return array|null The card (or list of cards), or null if there is no author.
     212         */
     213        function jf2_author_to_card( $author ) {
     214                if ( is_string( $author ) ) {
     215                        $author = trim( $author );
     216                        if ( '' === $author ) {
     217                                return null;
     218                        }
     219                        $key = ( preg_match( '#^https?://#i', $author ) && filter_var( $author, FILTER_VALIDATE_URL ) ) ? 'url' : 'name';
     220                        return array(
     221                                'type' => 'card',
     222                                $key   => $author,
     223                        );
     224                }
     225                if ( ! is_array( $author ) || empty( $author ) ) {
     226                        return null;
     227                }
     228                if ( wp_is_numeric_array( $author ) ) {
     229                        $cards = array_values( array_filter( array_map( __NAMESPACE__ . '\\jf2_author_to_card', $author ) ) );
     230                        if ( empty( $cards ) ) {
     231                                return null;
     232                        }
     233                        return ( 1 === count( $cards ) ) ? $cards[0] : $cards;
     234                }
     235                if ( ! isset( $author['type'] ) ) {
     236                        $author = array( 'type' => 'card' ) + $author;
     237                }
     238                return $author;
     239        }
     240}
     241
     242if ( ! function_exists( __NAMESPACE__ . '\\jf2_references' ) ) {
     243        /**
     244         * Moves nested objects into refs, per the jf2 spec.
     245         *
     246         * Any nested typed object with an http or https URL (an h-cite, h-card,
     247         * h-event, h-product or any other type) is replaced by its URL, and stored
     248         * in refs keyed by that URL, so the result is one level deep. The property
     249         * becomes a list of values, as before. Objects without such a URL stay in place, authors stay
     250         * cards, and a feed's items are left alone. References that a nested object
     251         * carries are merged into the same refs.
     252         *
     253         * @since 1.0.0
     254         * @since 2.0.0 Moves any typed object with a URL, not only citations, and
     255         *              merges nested refs.
     256         *
     257         * @link https://jf2.spec.indieweb.org/#references
     258         *
     259         * @param array $data jf2 object.
     260         * @return array The updated object.
     261         */
    140262        function jf2_references( $data ) {
     263                if ( ! is_array( $data ) ) {
     264                        return $data;
     265                }
     266                // Authors stay cards, and a feed's entries are not references.
     267                $skip = array( 'refs', 'author', 'items', 'children' );
    141268                foreach ( $data as $key => $val ) {
    142                         if ( ! is_array( $val ) ) {
     269                        if ( ! is_array( $val ) || in_array( $key, $skip, true ) || ( is_string( $key ) && '_' === substr( $key, 0, 1 ) ) ) {
    143270                                continue;
    144271                        }
    145                         if ( ! wp_is_numeric_array( $val ) ) {
    146                                 $val = array( $val );
    147                         }
    148                         if ( wp_is_numeric_array( $val ) ) {
    149                                 foreach ( $val as $value ) {
    150                                         // Indicates nested type
    151                                         if ( is_array( $value ) && array_key_exists( 'type', $value ) && 'cite' === $value['type'] ) {
    152                                                 if ( ! isset( $data['refs'] ) ) {
    153                                                         $data['refs'] = array();
    154                                                 }
    155                                                 if ( isset( $value['url'] ) ) {
    156                                                         $data['refs'][ $value['url'] ] = $value;
    157                                                         $data[ $key ]                  = array( $value['url'] );
    158                                                 }
     272                        $values  = wp_is_numeric_array( $val ) ? $val : array( $val );
     273                        $changed = false;
     274                        foreach ( $values as $i => $value ) {
     275                                if ( ! is_array( $value ) ) {
     276                                        continue;
     277                                }
     278                                // A nested object may carry references of its own: keep one flat refs.
     279                                if ( isset( $value['refs'] ) && is_array( $value['refs'] ) ) {
     280                                        foreach ( $value['refs'] as $ref_url => $ref ) {
     281                                                $data['refs'][ $ref_url ] = isset( $data['refs'][ $ref_url ] ) ? $data['refs'][ $ref_url ] + $ref : $ref;
    159282                                        }
    160                                         if ( 'category' === $key ) {
    161                                                 foreach ( $value as $k => $v ) {
    162                                                         if ( is_array( $v ) && array_key_exists( 'type', $v ) ) {
    163                                                                 if ( ! isset( $data['refs'] ) ) {
    164                                                                         $data['refs'] = array();
    165                                                                 }
    166                                                                 if ( isset( $v['url'] ) ) {
    167                                                                         $data['refs'][ $v['url'] ] = $v;
    168                                                                         $data['category'][ $k ]    = $v['url'];
    169                                                                 }
    170                                                         }
    171                                                 }
    172                                         }
    173                                 }
     283                                        unset( $value['refs'] );
     284                                        $values[ $i ] = $value;
     285                                        $changed      = true;
     286                                }
     287                                $url = $value['url'] ?? null;
     288                                if ( is_array( $url ) ) {
     289                                        $url = reset( $url );
     290                                }
     291                                // Any typed object with an http(s) URL is a reference. One without
     292                                // stays in place, where sanitizing can drop a bad URL and keep the rest.
     293                                if ( empty( $value['type'] ) || ! is_string( $value['type'] ) || ! is_string( $url ) || ! preg_match( '#^https?://#i', trim( $url ) ) ) {
     294                                        continue;
     295                                }
     296                                $data['refs'][ $url ] = isset( $data['refs'][ $url ] ) ? $data['refs'][ $url ] + $value : $value;
     297                                $values[ $i ]         = $url;
     298                                $changed              = true;
     299                        }
     300                        if ( $changed ) {
     301                                $data[ $key ] = $values;
    174302                        }
    175303                }
    … …  
    178306}
    179307
    180 if ( ! function_exists( 'url_to_author' ) ) {
    181         /**
    182          * Examine a url and try to determine the author ID it represents.
    183          *
    184          * @param string $url Permalink to check.
    185          *
    186          * @return WP_User, or null on failure.
     308if ( ! function_exists( __NAMESPACE__ . '\\url_to_author' ) ) {
     309        /**
     310         * Returns the local user that an author archive URL belongs to.
     311         *
     312         * Handles ?author=N URLs and pretty author permalinks.
     313         *
     314         * @since 1.0.0
     315         *
     316         * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
     317         *
     318         * @param string $url URL to check.
     319         * @return WP_User|false|null The user, false if no user has that ID or slug, or
     320         *                            null if the URL is not on this site or not an
     321         *                            author URL.
    187322         */
    188323        function url_to_author( $url ) {
    189324                global $wp_rewrite;
    190                 // check if url hase the same host
     325                // check if url hase the same host.
    191326                if ( wp_parse_url( site_url(), PHP_URL_HOST ) !== wp_parse_url( $url, PHP_URL_HOST ) ) {
    192327                        return null;
    193328                }
    194                 // first, check to see if there is a 'author=N' to match against
     329                // first, check to see if there is a 'author=N' to match against.
    195330                if ( preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
    196331                        $id = absint( $values[1] );
    … …  
    199334                        }
    200335                }
    201                 // check to see if we are using rewrite rules
     336                // check to see if we are using rewrite rules.
    202337                $rewrite = $wp_rewrite->wp_rewrite_rules();
    203                 // not using rewrite rules, and 'author=N' method failed, so we're out of options
     338                // not using rewrite rules, and 'author=N' method failed, so we're out of options.
    204339                if ( empty( $rewrite ) ) {
    205340                        return null;
    206341                }
    207                 // generate rewrite rule for the author url
     342                // generate rewrite rule for the author url.
    208343                $author_rewrite = $wp_rewrite->get_author_permastruct();
    209344                $author_regexp  = str_replace( '%author%', '', $author_rewrite );
    210                 // match the rewrite rule with the passed url
     345                // match the rewrite rule with the passed url.
    211346                if ( preg_match( '/https?:\/\/(.+)' . preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) {
    212347                        $user = get_user_by( 'slug', $match[2] );
    … …  
    219354}
    220355
    221 if ( ! function_exists( 'url_to_user' ) ) {
    222         /**
    223          * Get the user associated with a URL.
    224          *
    225          * @param string $url url to match
    226          * @return WP_User $user Associated user, or null if no associated user
     356if ( ! function_exists( __NAMESPACE__ . '\\url_to_user' ) ) {
     357        /**
     358         * Returns the local user associated with a URL.
     359         *
     360         * The site's home URL maps to the IndieWeb plugin's default author when the
     361         * IndieWeb plugin is active, otherwise to the only author if there is just
     362         * one. Author archive URLs map to their author, and other URLs are matched
     363         * against users' website field.
     364         *
     365         * @since 1.0.0
     366         *
     367         * @param string $url URL to check.
     368         * @return WP_User|false|null The user, or null (or false) if none matches.
    227369         */
    228370        function url_to_user( $url ) {
    … …  
    230372                        return null;
    231373                }
    232                 // Ensure has trailing slash
     374                // Ensure has trailing slash.
    233375                $url = trailingslashit( $url );
    234376                if ( ( 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ) ) && ( wp_parse_url( home_url(), PHP_URL_HOST ) === wp_parse_url( $url, PHP_URL_HOST ) ) ) {
    235377                        $url = set_url_scheme( $url, 'https' );
    236378                }
    237                 // Try to save the expense of a search query if the URL is the site URL
     379                // Try to save the expense of a search query if the URL is the site URL.
    238380                if ( home_url( '/' ) === $url ) {
    239                         // Use the Indieweb settings to set the default author
     381                        // Use the Indieweb settings to set the default author.
    240382                        if ( class_exists( 'Indieweb_Plugin' ) && ( get_option( 'iw_single_author' ) || ! is_multi_author() ) ) {
    241383                                return get_user_by( 'id', get_option( 'iw_default_author' ) );
    242384                        }
    243                         $users = get_users( array( 'who' => 'authors' ) );
     385                        $users = get_users( array( 'capability' => array( 'edit_posts' ) ) );
    244386                        if ( 1 === count( $users ) ) {
    245387                                return $users[0];
    … …  
    247389                        return null;
    248390                }
    249                 // Check if this is a author post URL
     391                // Check if this is a author post URL.
    250392                $user = url_to_author( $url );
    251                 if ( $user instanceof WP_User ) {
     393                if ( $user instanceof \WP_User ) {
    252394                        return $user;
    253395                }
    … …  
    257399                );
    258400                $users = get_users( $args );
    259                 // check result
     401                // check result.
    260402                if ( ! empty( $users ) ) {
    261403                        return $users[0];
    … …  
    265407}
    266408
    267 if ( ! function_exists( 'ifset' ) ) {
    268                 /**
    269                  * If set, return otherwise false.
    270                  *
    271                  * @param type $var Check if set.
    272                  * @return $var|false Return either $var or $return.
    273                  */
    274         function ifset( &$var, $return = false ) {
    275 
    276                         return isset( $var ) ? $var : $return;
    277         }
    278 }
    279 
    280 
    281 /*
    282  Inverse of wp_parse_url
    283  *
    284  * Slightly modified from p3k-utils (https://github.com/aaronpk/p3k-utils)
    285  * Copyright 2017 Aaron Parecki, used with permission under MIT License
    286  *
    287  * @link http://php.net/parse_url
    288  * @param  string $parsed_url the parsed URL (wp_parse_url)
    289  * @return string             the final URL
    290  */
    291 if ( ! function_exists( 'build_url' ) ) {
     409if ( ! function_exists( __NAMESPACE__ . '\\build_url' ) ) {
     410        /**
     411         * Builds a URL from its parts; the inverse of wp_parse_url().
     412         *
     413         * Slightly modified from p3k-utils. Copyright 2017 Aaron Parecki, used with
     414         * permission under the MIT License.
     415         *
     416         * @since 1.0.0
     417         *
     418         * @link https://github.com/aaronpk/p3k-utils
     419         * @link https://www.php.net/parse_url
     420         *
     421         * @param array $parsed_url URL components as returned by wp_parse_url().
     422         * @return string The URL.
     423         */
    292424        function build_url( $parsed_url ) {
    293425                        $scheme   = ! empty( $parsed_url['scheme'] ) ? $parsed_url['scheme'] . '://' : '';
    … …  
    306438
    307439
    308 if ( ! function_exists( 'normalize_url' ) ) {
    309         // Adds slash if no path is in the URL, and convert hostname to lowercase
     440if ( ! function_exists( __NAMESPACE__ . '\\normalize_url' ) ) {
     441        /**
     442         * Normalizes a URL for comparison.
     443         *
     444         * Lowercases the host and adds a / path if there is none.
     445         *
     446         * @since 1.0.0
     447         *
     448         * @param string $url   URL to normalize.
     449         * @param bool   $strip Optional. Whether to remove the query string. Default false.
     450         * @return string|null The normalized URL, or null if it has no host.
     451         */
    310452        function normalize_url( $url, $strip = false ) {
    311453                $parts = wp_parse_url( $url );
    … …  
    323465}
    324466
    325 if ( ! function_exists( 'normalize_iso8601' ) ) {
    326         // Tries to normalizes dates to a standard iso8601 string
     467if ( ! function_exists( __NAMESPACE__ . '\\normalize_iso8601' ) ) {
     468        /**
     469         * Normalizes a date string to W3C (ISO 8601) format.
     470         *
     471         * @since 1.0.0
     472         *
     473         * @param string|mixed $string Date string.
     474         * @return string|null The date in W3C format, the original string if it cannot
     475         *                     be parsed, or null if it is empty or not a string.
     476         */
    327477        function normalize_iso8601( $string ) {
    328                 $date = new DateTime( $string );
    329                 if ( $date ) {
    330                         return $date->format( DATE_W3C );
    331                 }
    332                 return $string;
    333         }
    334 }
    335 
    336 if ( ! function_exists( 'post_type_discovery' ) ) {
     478                if ( empty( $string ) || ! is_string( $string ) ) {
     479                        return null;
     480                }
     481                try {
     482                        $date = new \DateTime( $string );
     483                } catch ( \Exception $e ) {
     484                        return $string;
     485                }
     486                return $date->format( DATE_W3C );
     487        }
     488}
     489
     490if ( ! function_exists( __NAMESPACE__ . '\\pt_find_fragment_element' ) ) {
     491        /**
     492         * Finds the element a URL fragment points to.
     493         *
     494         * @since 2.0.0
     495         *
     496         * @param DOMDocument $doc Parsed page.
     497         * @param string      $id  Fragment, without the #.
     498         * @return DOMElement|null The element with that id, or null.
     499         */
     500        function pt_find_fragment_element( $doc, $id ) {
     501                if ( ! $doc instanceof \DOMDocument || ! is_string( $id ) || '' === $id ) {
     502                        return null;
     503                }
     504                $id = rawurldecode( $id );
     505                foreach ( $doc->getElementsByTagName( '*' ) as $element ) {
     506                        if ( $element->getAttribute( 'id' ) === $id ) {
     507                                return $element;
     508                        }
     509                }
     510                return null;
     511        }
     512}
     513
     514if ( ! function_exists( __NAMESPACE__ . '\\pt_effective_url' ) ) {
     515        /**
     516         * Returns the URL a response finally came from, after any redirects.
     517         *
     518         * A fragment on the requested URL (which is never sent to the server) is
     519         * kept, so that it can still pick out part of the page.
     520         *
     521         * @since 2.0.0
     522         *
     523         * @param string $url      Requested URL.
     524         * @param array  $response Response from pt_remote_get().
     525         * @return string The final URL, or $url if it isn't known.
     526         */
     527        function pt_effective_url( $url, $response ) {
     528                $object = ( is_array( $response ) && isset( $response['http_response'] ) && is_object( $response['http_response'] ) && method_exists( $response['http_response'], 'get_response_object' ) ) ? $response['http_response']->get_response_object() : null;
     529                if ( ! $object || empty( $object->url ) || ! is_string( $object->url ) || ! wp_http_validate_url( $object->url ) ) {
     530                        return $url;
     531                }
     532                $final    = $object->url;
     533                $fragment = wp_parse_url( $url, PHP_URL_FRAGMENT );
     534                if ( $fragment && ! wp_parse_url( $final, PHP_URL_FRAGMENT ) ) {
     535                        $final .= '#' . $fragment;
     536                }
     537                return $final;
     538        }
     539}
     540
     541if ( ! function_exists( __NAMESPACE__ . '\\pt_sniff_content_type' ) ) {
     542        /**
     543         * Works out a response's real format when its content type is missing,
     544         * generic or wrong.
     545         *
     546         * Feeds are often served as text/plain or text/html, and JSON as
     547         * text/plain or application/octet-stream. A body whose root element is
     548         * <rss>, <rdf:RDF> or <feed> is a feed whatever the header says (an HTML
     549         * page can't have those roots). A body that is a JSON object is checked
     550         * for JSON Feed and mf2 JSON when the header is missing or generic.
     551         *
     552         * @since 2.0.0
     553         *
     554         * @param string $content_type Content type from the response, without
     555         *                             parameters.
     556         * @param string $body         Response body.
     557         * @return string The content type to use: application/rss+xml,
     558         *                application/atom+xml, application/feed+json or
     559         *                application/mf2+json when sniffed, otherwise
     560         *                $content_type unchanged.
     561         */
     562        function pt_sniff_content_type( $content_type, $body ) {
     563                if ( ! is_string( $body ) || '' === $body ) {
     564                        return $content_type;
     565                }
     566                $known = array( 'application/rss+xml', 'application/atom+xml', 'application/feed+json', 'application/mf2+json', 'application/jf2+json', 'application/jf2feed+json' );
     567                if ( in_array( $content_type, $known, true ) ) {
     568                        return $content_type;
     569                }
     570                // Skip a byte order mark, the XML declaration, processing instructions, comments and a doctype.
     571                $start = ltrim( substr( $body, 0, 4096 ), "\xEF\xBB\xBF \t\r\n" );
     572                $start = preg_replace( '/^(\s*(<\?[^>]*\?>|<!--.*?-->|<!DOCTYPE[^>]*>))+\s*/is', '', $start );
     573                if ( preg_match( '/^<(rss|rdf:RDF)[\s>]/i', $start ) ) {
     574                        return 'application/rss+xml';
     575                }
     576                if ( preg_match( '/^<feed[\s>]/i', $start ) ) {
     577                        return 'application/atom+xml';
     578                }
     579                $generic = array( '', 'text/plain', 'application/octet-stream', 'text/html' );
     580                if ( in_array( $content_type, $generic, true ) && '{' === substr( $start, 0, 1 ) ) {
     581                        $json = json_decode( $body, true );
     582                        if ( is_array( $json ) && isset( $json['version'] ) && is_string( $json['version'] ) && 0 === strpos( $json['version'], 'https://jsonfeed.org/version/' ) ) {
     583                                return 'application/feed+json';
     584                        }
     585                        if ( is_array( $json ) && isset( $json['items'][0]['type'], $json['items'][0]['properties'] ) ) {
     586                                return 'application/mf2+json';
     587                        }
     588                }
     589                return $content_type;
     590        }
     591}
     592
     593if ( ! function_exists( __NAMESPACE__ . '\\name_is_content_prefix' ) ) {
     594        /**
     595         * Checks whether a name only repeats the start of the content.
     596         *
     597         * Ignores a trailing ellipsis (... or …) on the name, as on truncated
     598         * titles, and ignores whitespace in both.
     599         *
     600         * @since 2.0.0
     601         *
     602         * @param mixed $name    Name.
     603         * @param mixed $content Content text.
     604         * @return bool True if both are non-empty strings and the content starts
     605         *              with the name.
     606         */
     607        function name_is_content_prefix( $name, $content ) {
     608                if ( ! is_string( $name ) || ! is_string( $content ) ) {
     609                        return false;
     610                }
     611                $name    = preg_replace( '/\\s+/u', '', preg_replace( '/\\s*(\\.\\.\\.|…)\\s*$/u', '', $name ) );
     612                $content = preg_replace( '/\\s+/u', '', $content );
     613                return '' !== $name && '' !== $content && 0 === strpos( $content, $name );
     614        }
     615}
     616
     617if ( ! function_exists( __NAMESPACE__ . '\\post_type_discovery' ) ) {
     618        /**
     619         * Determines the IndieWeb post type of a jf2 or mf2 entry.
     620         *
     621         * Events, reviews and recipes are their own type. For an entry, response
     622         * properties decide the type first (review-of, rsvp, checkin, like-of,
     623         * in-reply-to, ...), then media (video, photo, audio). An entry with a name
     624         * that is not a prefix of its content (or summary) is an article; anything
     625         * else is a note.
     626         *
     627         * @since 1.0.0
     628         * @since 2.0.0 Adds review (from review-of, or the review type) and recipe,
     629         *              and follows the spec for a name without content.
     630         *
     631         * @link https://indieweb.org/post-type-discovery
     632         *
     633         * @param array|mixed $jf2 jf2 object, or an mf2 object (converted first).
     634         * @return string The post type, or an empty string for other types.
     635         */
    337636        function post_type_discovery( $jf2 ) {
    338637                if ( ! is_array( $jf2 ) ) {
    … …  
    345644                        return '';
    346645                }
    347                 if ( 'event' === $jf2['type'] ) {
    348                         return 'event';
     646                // Events (as in the spec), and reviews and recipes (as XRay does), are their own type.
     647                if ( in_array( $jf2['type'], array( 'event', 'review', 'recipe' ), true ) ) {
     648                        return $jf2['type'];
    349649                }
    350650                if ( 'entry' === $jf2['type'] ) {
    351651                        $map = array(
     652                                'review'    => array( 'review-of' ),
    352653                                'rsvp'      => array( 'rsvp' ),
    353654                                'checkin'   => array( 'checkin' ),
    … …  
    377678                                }
    378679                        }
    379                         if ( isset( $jf2['name'] ) && ! empty( $jf2['name'] ) ) {
    380                                 $jf2['name'] = $jf2['name'];
    381                                 $content     = ifset( $jf2['content'] );
    382                                 if ( ! $content ) {
    383                                         $content = ifset( $jf2['summary'] );
    384                                 }
    385                                 if ( is_array( $content ) && array_key_exists( 'text', $content ) ) {
    386                                         $content = $content['text'];
    387                                 }
    388                                 if ( is_string( $content ) ) {
    389                                         $content = trim( $content );
    390                                         if ( 0 !== strpos( $content, $jf2['name'] ) ) {
    391                                                 return 'article';
    392                                         }
    393                                 }
    394                         }
    395                                 return 'note';
     680                        // https://www.w3.org/TR/post-type-discovery/#algorithm: a name that is not
     681                        // a prefix of the content (or summary, or nothing) makes an article. A
     682                        // truncated name (ending in an ellipsis) is compared without it.
     683                        $name = ( isset( $jf2['name'] ) && is_string( $jf2['name'] ) ) ? trim( preg_replace( '/\s+/u', ' ', $jf2['name'] ) ) : '';
     684                        if ( '' !== $name ) {
     685                                $content = $jf2['content'] ?? null;
     686                                if ( is_array( $content ) ) {
     687                                        $content = $content['text'] ?? ( $content['value'] ?? null );
     688                                }
     689                                if ( ! is_string( $content ) || '' === trim( $content ) ) {
     690                                        $content = $jf2['summary'] ?? '';
     691                                }
     692                                $content = is_string( $content ) ? trim( preg_replace( '/\s+/u', ' ', $content ) ) : '';
     693                                if ( ! name_is_content_prefix( $name, $content ) ) {
     694                                        return 'article';
     695                                }
     696                        }
     697                        return 'note';
    396698                }
    397699                return '';
    … …  
    399701}
    400702
    401 if ( ! function_exists( 'seconds_to_iso8601' ) ) {
     703if ( ! function_exists( __NAMESPACE__ . '\\seconds_to_iso8601' ) ) {
     704        /**
     705         * Converts a number of seconds into an ISO 8601 duration.
     706         *
     707         * For example 3725 becomes PT1H2M5S.
     708         *
     709         * @since 1.0.0
     710         *
     711         * @param int|float $second Duration in seconds.
     712         * @return string ISO 8601 duration.
     713         */
    402714        function seconds_to_iso8601( $second ) {
    403715                $h   = intval( $second / 3600 );
    … …  
    418730}
    419731
    420 if ( ! function_exists( 'pt_load_domdocument' ) ) {
     732if ( ! function_exists( __NAMESPACE__ . '\\pt_load_domdocument' ) ) {
     733        /**
     734         * Parses HTML into a DOMDocument.
     735         *
     736         * On PHP 8.4 and later, uses PHP's own HTML5 parser (Dom\HTMLDocument),
     737         * which builds the same tree a browser does and is several times faster
     738         * than masterminds/html5 (see pt_native_html_document()). It can be turned
     739         * off with the parse_this_native_html_parser filter. Otherwise, or if that
     740         * fails, uses the bundled masterminds/html5 parser, and failing that PHP's
     741         * DOMDocument with errors suppressed.
     742         *
     743         * @since 1.0.0
     744         * @since 2.0.0 Uses PHP's HTML5 parser when available.
     745         *
     746         * @param string $content HTML.
     747         * @return DOMDocument The parsed document.
     748         */
    421749        function pt_load_domdocument( $content ) {
     750                $content = (string) $content;
     751                /**
     752                 * Filters whether to parse HTML with PHP's own HTML5 parser (PHP 8.4+).
     753                 *
     754                 * Return false to use masterminds/html5 even when PHP's parser is
     755                 * available. The two can differ on invalid markup, where PHP's parser
     756                 * follows the HTML5 algorithm (as browsers do).
     757                 *
     758                 * @since 2.0.0
     759                 *
     760                 * @param bool $enabled Whether to use it. Default true.
     761                 */
     762                if ( class_exists( 'Dom\HTMLDocument' ) && apply_filters( 'parse_this_native_html_parser', true ) ) {
     763                        $doc = pt_native_html_document( $content );
     764                        if ( $doc ) {
     765                                return $doc;
     766                        }
     767                }
    422768                if ( ! class_exists( '\Masterminds\HTML5', false ) ) {
    423769                        $file = plugin_dir_path( __DIR__ ) . 'lib/html5/autoloader.php';
    … …  
    430776                        $doc = $doc->loadHTML( $content );
    431777                } else {
    432                         $doc = new DOMDocument();
     778                        $doc = new \DOMDocument();
     779                        // DOMDocument::loadHTML() throws on an empty string in PHP 8.
     780                        if ( '' === trim( $content ) ) {
     781                                return $doc;
     782                        }
    433783                        libxml_use_internal_errors( true );
    434                         if ( function_exists( 'mb_convert_encoding' ) ) {
    435                                 $content = mb_convert_encoding( $content, 'HTML-ENTITIES', mb_detect_encoding( $content ) );
     784                        if ( function_exists( 'mb_encode_numericentity' ) ) {
     785                                // DOMDocument assumes ISO-8859-1, so convert to UTF-8 and encode non-ASCII characters as entities.
     786                                $encoding = mb_detect_encoding( $content, array( 'UTF-8', 'ISO-8859-1' ), true );
     787                                if ( $encoding && 'UTF-8' !== $encoding ) {
     788                                        $content = mb_convert_encoding( $content, 'UTF-8', $encoding );
     789                                }
     790                                $content = mb_encode_numericentity( $content, array( 0x80, 0x10FFFF, 0, 0x1FFFFF ), 'UTF-8' );
    436791                        }
    437792                        $doc->loadHTML( $content );
    … …  
    441796        }
    442797}
    443 if ( ! function_exists( 'pt_secure_rewrite' ) ) {
     798
     799if ( ! function_exists( __NAMESPACE__ . '\\pt_native_html_document' ) ) {
     800        /**
     801         * Parses HTML with PHP's HTML5 parser and returns it as a DOMDocument.
     802         *
     803         * Dom\HTMLDocument (PHP 8.4+) belongs to PHP's new DOM API, while php-mf2
     804         * and the rest of Parse This work with DOMDocument. The parsed tree is
     805         * passed across as XML, which reproduces it exactly without parsing the
     806         * HTML twice. Comments (which may contain "--") and attributes whose names
     807         * aren't valid XML (@click, x-on:click, :class) are removed first, since
     808         * XML can't hold them and nothing here reads them. Elements are kept out of
     809         * the XHTML namespace, so XPath queries such as //meta work as before.
     810         *
     811         * @since 2.0.0
     812         *
     813         * @param string $content HTML.
     814         * @return DOMDocument|null The parsed document, or null if PHP's parser
     815         *                          isn't available or the document can't be
     816         *                          passed across.
     817         */
     818        function pt_native_html_document( $content ) {
     819                if ( ! class_exists( 'Dom\HTMLDocument' ) ) {
     820                        return null;
     821                }
     822                try {
     823                        $html  = \Dom\HTMLDocument::createFromString( (string) $content, LIBXML_NOERROR | \Dom\HTML_NO_DEFAULT_NS );
     824                        $xpath = new \Dom\XPath( $html );
     825                        foreach ( iterator_to_array( $xpath->query( '//comment()' ) ) as $comment ) {
     826                                $comment->remove();
     827                        }
     828                        foreach ( iterator_to_array( $xpath->query( '//@*' ) ) as $attribute ) {
     829                                if ( ! preg_match( '/^[A-Za-z_][A-Za-z0-9_.\-]*$/', $attribute->name ) ) {
     830                                        $attribute->ownerElement->removeAttribute( $attribute->name ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHP DOM property.
     831                                }
     832                        }
     833                        $xml = $html->saveXml();
     834                } catch ( \Throwable $e ) {
     835                        return null;
     836                }
     837                $doc    = new \DOMDocument();
     838                $errors = libxml_use_internal_errors( true );
     839                $loaded = $doc->loadXML( $xml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_PARSEHUGE );
     840                libxml_clear_errors();
     841                libxml_use_internal_errors( $errors );
     842                return $loaded ? $doc : null;
     843        }
     844}
     845if ( ! function_exists( __NAMESPACE__ . '\\pt_remote_get' ) ) {
     846        /**
     847         * Retrieves a remote URL, retrying once with a browser user agent if the site rejects the request.
     848         *
     849         * Fetched documents decide which further URLs are requested (author pages,
     850         * alternates, REST API links), so this must stay on wp_safe_remote_get(),
     851         * which refuses local and private addresses. Don't switch it to
     852         * wp_remote_get().
     853         *
     854         * @param string $url         URL to retrieve.
     855         * @param array  $args        Optional. Arguments passed to wp_safe_remote_get().
     856         * @param array  $retry_codes Optional. Response codes that trigger the retry.
     857         * @return array|WP_Error The response, or WP_Error on failure.
     858         */
     859        function pt_remote_get( $url, $args = array(), $retry_codes = array( 403, 415 ) ) {
     860                $args = wp_parse_args(
     861                        $args,
     862                        array(
     863                                'timeout'             => 15,
     864                                'limit_response_size' => 1048576,
     865                                'redirection'         => 5,
     866                        )
     867                );
     868
     869                $response = wp_safe_remote_get( $url, $args );
     870                if ( is_wp_error( $response ) ) {
     871                        return $response;
     872                }
     873                if ( ! in_array( (int) wp_remote_retrieve_response_code( $response ), $retry_codes, true ) ) {
     874                        return $response;
     875                }
     876
     877                $args['user-agent'] = 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:57.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 Parse This/WP';
     878                $response           = wp_safe_remote_get( $url, $args );
     879                if ( is_wp_error( $response ) ) {
     880                        return $response;
     881                }
     882                $code = (int) wp_remote_retrieve_response_code( $response );
     883                if ( in_array( $code, $retry_codes, true ) ) {
     884                        return new \WP_Error( 'source_error', __( 'Unable to retrieve the URL.', 'parse-this' ), array( 'response_code' => $code ) );
     885                }
     886                return $response;
     887        }
     888}
     889
     890if ( ! function_exists( __NAMESPACE__ . '\\pt_secure_rewrite' ) ) {
     891        /**
     892         * Upgrades http:// URLs to https:// for hosts known to support HTTPS.
     893         *
     894         * @since 1.0.1
     895         *
     896         * @param string $url URL.
     897         * @return string The URL, with https:// if its domain is on the list.
     898         */
    444899        function pt_secure_rewrite( $url ) {
    445900                $host   = wp_parse_url( $url, PHP_URL_HOST );
    … …  
    460915                        'google.com',
    461916                        'gravatar.com',
    462                         'gstatic.com',
     917                        'gstatic.com', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- A domain to rewrite to https, not content loaded from it.
    463918                        'kernel.org',
    464919                        'lwn.net',
    465920                        'tumblr.com',
    466921                        'twitter.com',
     922                        'x.com',
    467923                        'vimeo.com',
    468924                        'wikipedia.org',
    … …  
    470926                        'youtube.com',
    471927                );
     928                /**
     929                 * Filters the domains whose http:// URLs are upgraded to https://.
     930                 *
     931                 * Matching is on the registrable domain, so listing example.com also
     932                 * covers www.example.com.
     933                 *
     934                 * @since 1.0.1
     935                 *
     936                 * @param string[] $secure Domains known to support HTTPS.
     937                 */
    472938                $secure = apply_filters( 'pt_rewrite_secure', $secure );
    473939                if ( in_array( $host, $secure, true ) ) {
    … …  
    479945
    480946
    481 /**
    482  * Parses Link Headers
    483  *
    484  *
    485  * @return array
    486  */
    487 
    488 if ( ! function_exists( 'pt_parse_header_links' ) ) {
     947if ( ! function_exists( __NAMESPACE__ . '\\pt_parse_header_links' ) ) {
     948        /**
     949         * Parses HTTP Link headers.
     950         *
     951         * @since 2.0.0
     952         *
     953         * @link https://www.rfc-editor.org/rfc/rfc8288
     954         *
     955         * @param string|string[] $links One Link header value, or several. A value may
     956         *                               hold multiple comma-separated links.
     957         * @return array[] One entry per link and rel value, with 'uri', 'rel' and any
     958         *                 other parameters (type, title, ...).
     959         */
    489960        function pt_parse_header_links( $links ) {
    490961                $items = array();
    491962
     963                if ( is_string( $links ) ) {
     964                        $links = array( $links );
     965                }
     966
    492967                if ( is_array( $links ) && 1 <= count( $links ) ) {
     968                        // A header may hold several comma-separated links. Only split on commas that start a new <uri>, as URIs may contain commas.
     969                        $split = array();
    493970                        foreach ( $links as $link ) {
    494                                 $item   = array();
    495                                 $pieces = explode( ';', $link );
    496                                 $uri    = array_shift( $pieces );
     971                                $split = array_merge( $split, preg_split( '/,(?=\s*<)/', $link ) );
     972                        }
     973                        foreach ( $split as $link ) {
     974                                $item = array();
     975                                if ( preg_match( '/^\s*<([^>]*)>(.*)$/s', $link, $match ) ) {
     976                                        $uri    = $match[1];
     977                                        $pieces = explode( ';', $match[2] );
     978                                } else {
     979                                        $pieces = explode( ';', $link );
     980                                        $uri    = array_shift( $pieces );
     981                                }
    497982                                foreach ( $pieces as $p ) {
    498                                         $elements = explode( '=', $p );
    499 
    500                                         $item[ trim( $elements[0] ) ] = trim( $elements[1], '"\'' );
     983                                        $elements = explode( '=', $p, 2 );
     984                                        $name     = trim( $elements[0] );
     985                                        if ( '' === $name ) {
     986                                                continue;
     987                                        }
     988
     989                                        $item[ $name ] = isset( $elements[1] ) ? trim( $elements[1], " \t\"'" ) : '';
    501990                                }
    502991
    … …  
    5191008}
    5201009
    521 /**
    522  * Find WordPress REST Alternate
    523  *
    524  *
    525  * @return string|false
    526  */
    527 
    528 if ( ! function_exists( 'pt_find_rest_alternate' ) ) {
     1010if ( ! function_exists( __NAMESPACE__ . '\\pt_find_rest_alternate' ) ) {
     1011        /**
     1012         * Finds the application/json alternate link among parsed Link headers.
     1013         *
     1014         * WordPress sends this for each post, pointing at its REST API URL.
     1015         *
     1016         * @since 2.0.0
     1017         *
     1018         * @param array[] $links Links from pt_parse_header_links().
     1019         * @return string|false The URL, or false if there is none.
     1020         */
    5291021        function pt_find_rest_alternate( $links ) {
    5301022                foreach ( $links as $link ) {
    531                         if ( 'alternate' === ifset( $link['rel'] ) && 'application/json' === ifset( $link['type'] ) ) {
     1023                        if ( 'alternate' === ( $link['rel'] ?? null ) && 'application/json' === ( $link['type'] ?? null ) ) {
    5321024                                return $link['uri'];
    5331025                        }
    … …  
    5381030}
    5391031
    540 /**
    541  * Find WordPress REST Endpoint
    542  *
    543  *
    544  * @return string|false
    545  */
    546 
    547 if ( ! function_exists( 'pt_find_rest_endpoint' ) ) {
     1032if ( ! function_exists( __NAMESPACE__ . '\\pt_find_rest_endpoint' ) ) {
     1033        /**
     1034         * Finds the WordPress REST API root among parsed Link headers.
     1035         *
     1036         * @since 2.0.0
     1037         *
     1038         * @param array[] $links Links from pt_parse_header_links().
     1039         * @return string|false The REST API root URL, or false if there is none.
     1040         */
    5481041        function pt_find_rest_endpoint( $links ) {
    5491042                foreach ( $links as $link ) {
    550                         if ( 'https://api.w.org/' === ifset( $link['rel'] ) ) {
     1043                        if ( 'https://api.w.org/' === ( $link['rel'] ?? null ) ) {
    5511044                                return $link['uri'];
    5521045                        }
    … …  
    5561049}
    5571050
    558 if ( ! function_exists( 'pt_make_absolute_url' ) ) {
     1051if ( ! function_exists( __NAMESPACE__ . '\\pt_absolute_urls_in_html' ) ) {
     1052        /**
     1053         * Makes the links and media URLs in an HTML fragment absolute.
     1054         *
     1055         * Resolves href, src and poster attributes against a base URL, for content
     1056         * taken from a document (such as a JSON Feed item) whose own URL differs
     1057         * from where it will be shown.
     1058         *
     1059         * @since 2.0.0
     1060         *
     1061         * @param string $html HTML fragment.
     1062         * @param string $base Base URL.
     1063         * @return string The HTML with absolute URLs, or $html unchanged if it has
     1064         *                none to resolve or $base is not a URL.
     1065         */
     1066        function pt_absolute_urls_in_html( $html, $base ) {
     1067                if ( ! is_string( $html ) || '' === trim( $html ) || ! wp_http_validate_url( $base ) || ! preg_match( '/\s(href|src|poster)\s*=/i', $html ) ) {
     1068                        return $html;
     1069                }
     1070                $doc  = pt_load_domdocument( '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>' . $html . '</body></html>' );
     1071                $body = $doc->getElementsByTagName( 'body' )->item( 0 );
     1072                if ( ! $body ) {
     1073                        return $html;
     1074                }
     1075                foreach ( ( new \DOMXPath( $doc ) )->query( '//body//*[@href or @src or @poster]' ) as $element ) {
     1076                        foreach ( array( 'href', 'src', 'poster' ) as $attribute ) {
     1077                                $value = $element->getAttribute( $attribute );
     1078                                if ( '' !== $value && '#' !== $value[0] ) {
     1079                                        $element->setAttribute( $attribute, pt_make_absolute_url( $value, $base ) );
     1080                                }
     1081                        }
     1082                }
     1083                $out = '';
     1084                foreach ( $body->childNodes as $node ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHP DOM property.
     1085                        $out .= $doc->saveHTML( $node );
     1086                }
     1087                return $out;
     1088        }
     1089}
     1090
     1091if ( ! function_exists( __NAMESPACE__ . '\\pt_make_absolute_url' ) ) {
     1092        /**
     1093         * Resolves a relative URL against a base URL.
     1094         *
     1095         * @since 2.0.0
     1096         *
     1097         * @param string $maybe_relative_path URL or path to resolve.
     1098         * @param string $url                 Base URL.
     1099         * @return string The absolute URL, or $maybe_relative_path unchanged if it is
     1100         *                already absolute or either value cannot be parsed.
     1101         */
    5591102        function pt_make_absolute_url( $maybe_relative_path, $url ) {
    5601103                if ( empty( $url ) ) {
    … …  
    5921135                        }
    5931136                }
    594                
     1137
    5951138                // Start off with the absolute URL path.
    5961139                $path = ! empty( $url_parts['path'] ) ? $url_parts['path'] : '/';
    … …  
    6001143                        $path = $relative_url_parts['path'];
    6011144
    602                 // Else it's a relative path.
     1145                        // Else it's a relative path.
    6031146                } elseif ( ! empty( $relative_url_parts['path'] ) ) {
    6041147                        // Strip off any file components from the absolute path.
    … …  
    6271170                }
    6281171
    629 
    6301172                return $absolute_path . '/' . ltrim( $path, '/' );
    6311173        }
  • indieweb-post-kinds/trunk/lib/parse-this/lib/html5/HTML5.php

    r2351228 r3738687  
    147147     *
    148148     * @param string $input
    149      * @param array  $options
    150149     *
    151150     * @return \DOMDocument
  • indieweb-post-kinds/trunk/lib/parse-this/lib/html5/HTML5/Elements.php

    r2110745 r3738687  
    7171     */
    7272    const BLOCK_ONLY_INLINE = 128;
     73
     74    /**
     75     * Elements with optional end tags that cause auto-closing of previous and parent tags,
     76     * as example most of the table related tags, see https://www.w3.org/TR/html401/struct/tables.html
     77     * Structure is as follows:
     78     * TAG-NAME => [PARENT-TAG-NAME-TO-CLOSE1, PARENT-TAG-NAME-TO-CLOSE2, ...].
     79     *
     80     * Order is important, after auto-closing one parent with might have to close also their parent.
     81     *
     82     * @var array<string, string[]>
     83     */
     84    public static $optionalEndElementsParentsToClose = array(
     85        'tr' => array('td', 'tr'),
     86        'td' => array('td', 'th'),
     87        'th' => array('td', 'th'),
     88        'tfoot' => array('td', 'th', 'tr', 'tbody', 'thead'),
     89        'tbody' => array('td', 'th', 'tr', 'thead'),
     90    );
    7391
    7492    /**
    … …  
    186204        'ul' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
    187205        'var' => 1,
    188         'video' => 65, // NORMAL | BLOCK_TAG
     206        'video' => 1,
    189207        'wbr' => 9, // NORMAL | VOID_TAG
    190208
    … …  
    293311        'feDisplacementMap' => 1,
    294312        'feDistantLight' => 1,
     313        'feDropShadow' => 1,
    295314        'feFlood' => 1,
    296315        'feFuncA' => 1,
    … …  
    446465        'fedisplacementmap' => 'feDisplacementMap',
    447466        'fedistantlight' => 'feDistantLight',
     467        'fedropshadow' => 'feDropShadow',
    448468        'feflood' => 'feFlood',
    449469        'fefunca' => 'feFuncA',
  • indieweb-post-kinds/trunk/lib/parse-this/lib/html5/HTML5/Parser/CharacterReference.php

    r2110745 r3738687  
    4949
    5050    /**
    51      * Given a hexidecimal number, return the UTF-8 character.
     51     * Given a hexadecimal number, return the UTF-8 character.
    5252     *
    5353     * @param $hexdec
  • indieweb-post-kinds/trunk/lib/parse-this/lib/html5/HTML5/Parser/DOMTreeBuilder.php

    r2623705 r3738687  
    232232     * This is used for handling Processor Instructions as they are
    233233     * inserted. If omitted, PI's are inserted directly into the DOM tree.
    234      *
    235      * @param InstructionProcessor $proc
    236234     */
    237235    public function setInstructionProcessor(InstructionProcessor $proc)
    … …  
    360358        }
    361359
     360        // some elements as table related tags might have optional end tags that force us to auto close multiple tags
     361        // https://www.w3.org/TR/html401/struct/tables.html
     362        if ($this->current instanceof \DOMElement && isset(Elements::$optionalEndElementsParentsToClose[$lname])) {
     363            foreach (Elements::$optionalEndElementsParentsToClose[$lname] as $parentElName) {
     364                if ($this->current instanceof \DOMElement && $this->current->tagName === $parentElName) {
     365                    $this->autoclose($parentElName);
     366                }
     367            }
     368        }
     369
    362370        try {
    363371            $prefix = ($pos = strpos($lname, ':')) ? substr($lname, 0, $pos) : '';
    … …  
    390398        if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) {
    391399            // PHP tends to free the memory used by DOM,
    392             // to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes
     400            // to avoid spl_object_id collisions we have to avoid garbage collection of $ele storing it into $pushes
    393401            // see https://bugs.php.net/bug.php?id=67459
    394             $this->pushes[spl_object_hash($ele)] = array($pushes, $ele);
     402            $this->pushes[spl_object_id($ele)] = array($pushes, $ele);
    395403        }
    396404
    … …  
    514522        }
    515523
    516         $cid = spl_object_hash($this->current);
     524        $cid = spl_object_id($this->current);
    517525
    518526        // XXX: HTML has no parent. What do we do, though,
  • indieweb-post-kinds/trunk/lib/parse-this/lib/html5/HTML5/Parser/StringInputStream.php

    r2110745 r3738687  
    184184     * @return string The current character.
    185185     */
     186    #[\ReturnTypeWillChange]
    186187    public function current()
    187188    {
    … …  
    193194     * This is part of the Iterator interface.
    194195     */
     196    #[\ReturnTypeWillChange]
    195197    public function next()
    196198    {
    … …  
    201203     * Rewind to the start of the string.
    202204     */
     205    #[\ReturnTypeWillChange]
    203206    public function rewind()
    204207    {
    … …  
    211214     * @return bool Whether the current pointer location is valid.
    212215     */
     216    #[\ReturnTypeWillChange]
    213217    public function valid()
    214218    {
    … …  
    325329    }
    326330
     331    #[\ReturnTypeWillChange]
    327332    public function key()
    328333    {
  • indieweb-post-kinds/trunk/lib/parse-this/lib/html5/HTML5/Parser/Tokenizer.php

    r2839509 r3738687  
    132132            $tok = $this->scanner->next();
    133133
    134             if ('!' === $tok) {
     134            if (false === $tok) {
     135                // end of string
     136                $this->parseError('Illegal tag opening');
     137            } elseif ('!' === $tok) {
    135138                $this->markupDeclaration();
    136139            } elseif ('/' === $tok) {
    … …  
    138141            } elseif ('?' === $tok) {
    139142                $this->processingInstruction();
    140             } elseif (ctype_alpha($tok)) {
     143            } elseif ($this->is_alpha($tok)) {
    141144                $this->tagName();
    142145            } else {
    … …  
    348351        // EOF -> parse error
    349352        // -> parse error
    350         if (!ctype_alpha($tok)) {
     353        if (!$this->is_alpha($tok)) {
    351354            $this->parseError("Expected tag name, got '%s'", $tok);
    352355            if ("\0" == $tok || false === $tok) {
    … …  
    505508
    506509        $val = $this->attributeValue();
    507         if ($isValidAttribute) {
     510        if ($isValidAttribute && !array_key_exists($name, $attributes)) {
    508511            $attributes[$name] = $val;
    509512        }
    … …  
    727730        if ('!' == $this->scanner->current() && '>' == $this->scanner->peek()) {
    728731            $this->scanner->consume(); // Consume the last '>'
     732
    729733            return true;
    730734        }
    … …  
    11251129            }
    11261130
    1127             // Hexidecimal encoding.
     1131            // Hexadecimal encoding.
    11281132            // X[0-9a-fA-F]+;
    11291133            // x[0-9a-fA-F]+;
    … …  
    11951199        return '&';
    11961200    }
     1201
     1202    /**
     1203     * Checks whether a (single-byte) character is an ASCII letter or not.
     1204     *
     1205     * @param string $input A single-byte string
     1206     *
     1207     * @return bool True if it is a letter, False otherwise
     1208     */
     1209    protected function is_alpha($input)
     1210    {
     1211        if (!is_string($input) || 1 !== strlen($input)) {
     1212            return false;
     1213        }
     1214
     1215        $code = ord($input);
     1216
     1217        return ($code >= 97 && $code <= 122) || ($code >= 65 && $code <= 90);
     1218    }
    11971219}
  • indieweb-post-kinds/trunk/lib/parse-this/lib/html5/HTML5/Parser/TreeBuildingRules.php

    r2110745 r3738687  
    8181            case 'tfoot':
    8282            case 'table': // Spec isn't explicit about this, but it's necessary.
    83 
    8483                return $this->closeIfCurrentMatches($new, $current, array(
    8584                    'thead',
  • indieweb-post-kinds/trunk/lib/parse-this/lib/html5/HTML5/Parser/UTF8Utils.php

    r2110745 r3738687  
    3939    /**
    4040     * Count the number of characters in a string.
    41      * UTF-8 aware. This will try (in order) iconv, MB, libxml, and finally a custom counter.
     41     * UTF-8 aware. This will try (in order) iconv, MB, and finally a custom counter.
    4242     *
    4343     * @param string $string
    … …  
    5454        if (function_exists('iconv_strlen')) {
    5555            return iconv_strlen($string, 'utf-8');
    56         }
    57 
    58         if (function_exists('utf8_decode')) {
    59             // MPB: Will this work? Won't certain decodes lead to two chars
    60             // extrapolated out of 2-byte chars?
    61             return strlen(utf8_decode($string));
    6256        }
    6357
  • indieweb-post-kinds/trunk/lib/parse-this/lib/html5/HTML5/Serializer/OutputRules.php

    r2351228 r3738687  
    1717class OutputRules implements RulesInterface
    1818{
    19     /**
    20      * Defined in http://www.w3.org/TR/html51/infrastructure.html#html-namespace-0.
    21      */
    2219    const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml';
    2320
    … …  
    5047
    5148    const IM_IN_MATHML = 3;
    52 
    53     /**
    54      * Used as cache to detect if is available ENT_HTML5.
    55      *
    56      * @var bool
    57      */
    58     private $hasHTML5 = false;
    5949
    6050    protected $traverser;
    … …  
    168158        $this->outputMode = static::IM_IN_HTML;
    169159        $this->out = $output;
    170         $this->hasHTML5 = defined('ENT_HTML5');
    171160    }
    172161
    … …  
    207196    }
    208197
     198    /**
     199     * @param \DOMElement $ele
     200     */
    209201    public function element($ele)
    210202    {
    … …  
    228220
    229221        $this->openTag($ele);
     222        // The tag is already self-closed (`<svg />` or `<math />`) in `openTag` if there are no child nodes.
     223        $handledAsVoidTag = $this->outputMode !== static::IM_IN_HTML && !$ele->hasChildNodes();
     224
    230225        if (Elements::isA($name, Elements::TEXT_RAW)) {
    231226            foreach ($ele->childNodes as $child) {
    … …  
    249244
    250245        // If not unary, add a closing tag.
    251         if (!Elements::isA($name, Elements::VOID_TAG)) {
     246        if (!$handledAsVoidTag && !Elements::isA($name, Elements::VOID_TAG)) {
    252247            $this->closeTag($ele);
    253248        }
    … …  
    449444    protected function wr($text)
    450445    {
    451         fwrite($this->out, $text);
     446        fwrite($this->out, (string) $text);
    452447
    453448        return $this;
    … …  
    484479     *      encoding here will just escape &'<>".
    485480     *
    486      *      Note, PHP 5.4+ has better html5 encoding.
    487      *
    488      * @todo Use the Entities class in php 5.3 to have html5 entities.
    489      *
    490481     * @param string $text      Text to encode.
    491      * @param bool   $attribute True if we are encoding an attrubute, false otherwise.
     482     * @param bool   $attribute True if we are encoding an attribute, false otherwise.
    492483     *
    493484     * @return string The encoded text.
    … …  
    500491        }
    501492
    502         // If we are in PHP 5.4+ we can use the native html5 entity functionality to
    503         // convert the named character references.
    504 
    505         if ($this->hasHTML5) {
    506             return htmlentities($text, ENT_HTML5 | ENT_SUBSTITUTE | ENT_QUOTES, 'UTF-8', false);
    507         }         // If a version earlier than 5.4 html5 entities are not entirely handled.
    508         // This manually handles them.
    509         else {
    510             return strtr($text, HTML5Entities::$map);
    511         }
     493        return htmlentities($text, ENT_HTML5 | ENT_SUBSTITUTE | ENT_QUOTES, 'UTF-8', false);
    512494    }
    513495
  • indieweb-post-kinds/trunk/lib/parse-this/lib/mf2/Parser.php

    r2839509 r3738687  
    146146
    147147/**
    148  * Registered with the XPath object and used within XPaths for finding root elements.
    149  * @param string $class
    150  * @return bool
    151  */
    152 function classHasMf2RootClassname($class) {
    153         return count(mfNamesFromClass($class, 'h-')) > 0;
    154 }
    155 
    156 /**
    157148 * Get Nested µf Property Name From Class
    158149 *
    … …  
    365356         */
    366357        public function __construct($input, $url = null, $jsonMode = false) {
    367                 $emptyDocDefault = '<html><body></body></html>';
    368358                libxml_use_internal_errors(true);
    369359                if (is_string($input)) {
    370                         if (empty($input)) {
    371                                         $input = $emptyDocDefault;
    372                         }
    373                                
    374360                        if (class_exists('Masterminds\\HTML5')) {
    375361                                        $doc = new \Masterminds\HTML5(array('disable_html_ns' => true));
    … …  
    383369                } else {
    384370                        $doc = new DOMDocument();
    385                         @$doc->loadHTML($emptyDocDefault);
    386                 }
    387 
    388                 // Create an XPath object and allow some PHP functions to be used within XPath queries.
     371                        @$doc->loadHTML('');
     372                }
     373
    389374                $this->xpath = new DOMXPath($doc);
    390                 $this->xpath->registerNamespace('php', 'http://php.net/xpath');
    391                 $this->xpath->registerPhpFunctions('\\Mf2\\classHasMf2RootClassname');
    392375
    393376                $baseurl = $url;
    … …  
    11821165                        'properties' => $return
    11831166                );
    1184 
     1167               
    11851168                if(trim($e->getAttribute('id')) !== '') {
    11861169                        $parsed['id'] = trim($e->getAttribute("id"));
    … …  
    15241507                // start with mf2 root class name xpath
    15251508                $xpaths = array(
    1526                         '(php:function("\\Mf2\\classHasMf2RootClassname", normalize-space(@class)))'
     1509                        'contains(concat(" ",normalize-space(@class)), " h-")'
    15271510                );
    15281511
    … …  
    15791562                                break;
    15801563
    1581                                 case 'hfeed':
    1582                                         $this->upgradeRelTagToCategory($el);
    1583                                 break;
    1584 
    1585                                 case 'hproduct':
    1586                                         $review_and_hreview_aggregate = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " review ") and contains(concat(" ", normalize-space(@class), " "), " hreview-aggregate ")]', $el);
    1587 
    1588                                         if ( $review_and_hreview_aggregate->length ) {
    1589                                                 foreach ( $review_and_hreview_aggregate as $tempEl ) {
    1590                                                         if ( !$this->hasRootMf2($tempEl) ) {
    1591                                                                 $this->backcompat($tempEl, 'hreview-aggregate');
    1592                                                                 $this->addMfClasses($tempEl, 'p-review h-review-aggregate');
    1593                                                                 $this->addUpgraded($tempEl, array('review hreview-aggregate'));
    1594                                                         }
    1595                                                 }
    1596                                         }
    1597 
    1598                                         $review_and_hreview = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " review ") and contains(concat(" ", normalize-space(@class), " "), " hreview ")]', $el);
    1599 
    1600                                         if ( $review_and_hreview->length ) {
    1601                                                 foreach ( $review_and_hreview as $tempEl ) {
    1602                                                         if ( !$this->hasRootMf2($tempEl) ) {
    1603                                                                 $this->backcompat($tempEl, 'hreview');
    1604                                                                 $this->addMfClasses($tempEl, 'p-review h-review');
    1605                                                                 $this->addUpgraded($tempEl, array('review hreview'));
    1606                                                         }
    1607                                                 }
    1608                                         }
    1609 
    1610                                 break;
    1611 
    1612                                 case 'hreview-aggregate':
    16131564                                case 'hreview':
    16141565                                        $item_and_vcard = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " item ") and contains(concat(" ", normalize-space(@class), " "), " vcard ")]', $el);
    … …  
    16521603
    16531604                                case 'vevent':
    1654                                         $location_and_vcard = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " location ") and contains(concat(" ", normalize-space(@class), " "), " vcard ")]', $el);
    1655 
    1656                                         if ( $location_and_vcard->length ) {
    1657                                                 foreach ( $location_and_vcard as $tempEl ) {
     1605                                        $location = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " location ")]', $el);
     1606
     1607                                        if ( $location->length ) {
     1608                                                foreach ( $location as $tempEl ) {
    16581609                                                        if ( !$this->hasRootMf2($tempEl) ) {
    1659                                                                 $this->addMfClasses($tempEl, 'p-location h-card');
     1610                                                                $this->addMfClasses($tempEl, 'h-card');
    16601611                                                                $this->backcompat($tempEl, 'vcard');
    16611612                                                        }
    … …  
    17371688        public function hasRootMf2(\DOMElement $el) {
    17381689                $class = str_replace(array("\t", "\n"), ' ', $el->getAttribute('class'));
    1739 
    1740                 // Check for valid mf2 root classnames, not just any classname with a h- prefix.
    1741                 return count(mfNamesFromClass($class, 'h-')) > 0;
     1690                $classes = array_filter(explode(' ', $class));
     1691
     1692                foreach ( $classes as $classname ) {
     1693                        if ( strpos($classname, 'h-') === 0 ) {
     1694                                return true;
     1695                        }
     1696                }
     1697
     1698                return false;
    17421699        }
    17431700
    … …  
    17991756                'vevent' => 'h-event',
    18001757                'hreview' => 'h-review',
    1801                 'hreview-aggregate' => 'h-review-aggregate',
    18021758                'hproduct' => 'h-product',
    18031759                'adr' => 'h-adr',
    1804                 'geo' => 'h-geo'
    18051760        );
    18061761
    … …  
    19191874                ),
    19201875                'hfeed' => array(
    1921                         'author' => array(
    1922                                 'replace' => 'p-author h-card',
    1923                                 'context' => 'vcard'
    1924                         ),
    1925                         'url' => array(
    1926                                 'replace' => 'u-url'
    1927                         ),
    1928                         'photo' => array(
    1929                                 'replace' => 'u-photo'
    1930                         ),
    1931                         'category' => array(
    1932                                 'replace' => 'p-category'
    1933                         ),
     1876                        # nothing currently
    19341877                ),
    19351878                'hentry' => array(
    … …  
    20411984                        ),
    20421985                        'location' => array(
    2043                                 'replace' => 'p-location',
     1986                                'replace' => 'h-card',
     1987                                'context' => 'vcard'
    20441988                        ),
    20451989                        'geo' => array(
    20461990                                'replace' => 'p-location h-geo'
    20471991                        ),
    2048                         'attendee' => array(
    2049                                 'replace' => 'p-attendee h-card',
    2050                                 'context' => 'vcard'
    2051                         )
    20521992                ),
    20531993                'hreview' => array(
    … …  
    20852025                        ),
    20862026                ),
    2087                 'hreview-aggregate' => array(
    2088                         'summary' => array(
    2089                                 'replace' => 'p-name'
    2090                         ),
    2091                         # fn: see item.fn below
    2092                         # photo: see item.photo below
    2093                         # url: see item.url below
    2094                         'item' => array(
    2095                                 'replace' => 'p-item h-item',
    2096                                 'context' => 'item'
    2097                         ),
    2098                         'rating' => array(
    2099                                 'replace' => 'p-rating'
    2100                         ),
    2101                         'best' => array(
    2102                                 'replace' => 'p-best'
    2103                         ),
    2104                         'worst' => array(
    2105                                 'replace' => 'p-worst'
    2106                         ),
    2107                         'average' => array(
    2108                                 'replace' => 'p-average'
    2109                         ),
    2110                         'count' => array(
    2111                                 'replace' => 'p-count'
    2112                         ),
    2113                         'votes' => array(
    2114                                 'replace' => 'p-votes'
    2115                         ),
    2116                 ),
    21172027                'hproduct' => array(
    21182028                        'fn' => array(
    … …  
    21372047                                'replace' => 'u-url',
    21382048                        ),
    2139                         // review is handled in the special processing section to allow for 'review hreview-aggregate'
     2049                        'review' => array(
     2050                                'replace' => 'p-review h-review',
     2051                        ),
    21402052                        'price' => array(
    21412053                                'replace' => 'p-price'
  • indieweb-post-kinds/trunk/lib/parse-this/parse-this.php

    r2507837 r3738687  
    33 * Plugin Name: Parse This
    44 * Plugin URI: https://github.com/dshanske/parse-this
    5  * Description:
    6  * Version: 1.0.1
     5 * Description: Turns URLs into structured jf2 data from microformats2, JSON-LD, meta tags and RSS, Atom and JSON feeds.
     6 * Version: 2.0.0
    77 * Author: David Shanske
    88 * Author URI: https://david.shanske.com
     9 * Requires at least: 6.2
     10 * Requires PHP: 7.4
     11 * License: GPLv2 or later
     12 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
    913 * Text Domain: parse-this
    10  * Domain Path:  /languages
     14 *
     15 * @package Parse_This
    1116 */
    1217
    13 
    14 /*
    15  Parse This Load
    16  */
     18defined( 'ABSPATH' ) || exit;
    1719
    1820if ( ! function_exists( 'parse_this_loader' ) ) {
     21        /**
     22         * Loads Parse This.
     23         *
     24         * Runs on plugins_loaded at priority 9, so that the standalone plugin loads
     25         * ahead of copies bundled in other plugins (Post Kinds loads its copy at
     26         * priority 11). Functions are guarded by function_exists() and the first
     27         * autoloader registered serves the classes, so whichever copy loads first
     28         * wins. It also registers the deprecated global names in aliases.php, so
     29         * that plugins bundling an older copy use this one.
     30         *
     31         * Defines PARSE_THIS_VERSION for the copy that loads, and fires
     32         * parse_this_loaded once everything is available. A copy older than 2.0.0
     33         * defines neither, so a plugin can tell when an older bundled copy won.
     34         *
     35         * @since 1.0.0
     36         * @since 2.0.0 Defines PARSE_THIS_VERSION and fires parse_this_loaded.
     37         */
    1938        function parse_this_loader() {
     39                if ( ! defined( 'PARSE_THIS_VERSION' ) ) {
     40                        /**
     41                         * Version of the copy of Parse This that loaded. Keep in step with the
     42                         * Version header.
     43                         *
     44                         * @since 2.0.0
     45                         * @var string
     46                         */
     47                        define( 'PARSE_THIS_VERSION', '2.0.0' );
     48                }
    2049                require_once plugin_dir_path( __FILE__ ) . 'includes/autoload.php';
     50                require_once plugin_dir_path( __FILE__ ) . 'includes/functions.php';
    2151
    22                 // Functions Not Available in Earlier Versions of WordPress
    23                 require_once plugin_dir_path( __FILE__ ) . 'includes/compat-functions.php';
     52                new ParseThis\REST_Endpoint();
    2453
    25                 require_once plugin_dir_path( __FILE__ ) . 'includes/functions.php';
    26                 // Parse This REST Endpoint
    27                 require_once plugin_dir_path( __FILE__ ) . 'includes/class-rest-parse-this.php';
    28 
     54                /**
     55                 * Fires once Parse This has loaded: its classes, functions and REST route
     56                 * are available.
     57                 *
     58                 * Plugins that use Parse This can hook here, or check PARSE_THIS_VERSION,
     59                 * rather than depend on load order.
     60                 *
     61                 * @since 2.0.0
     62                 *
     63                 * @param string $version Version of the copy of Parse This that loaded.
     64                 */
     65                do_action( 'parse_this_loaded', PARSE_THIS_VERSION );
    2966        }
    3067        add_action( 'plugins_loaded', 'parse_this_loader', 9 );
    3168}
    32 
  • indieweb-post-kinds/trunk/lib/parse-this/readme.txt

    r2507837 r3738687  
    11=== Parse This ===
    22Contributors: dshanske
    3 Tags: indieweb
    4 Stable tag: trunk
    5 Requires at least: 4.9
    6 Requires PHP: 5.6
    7 Tested up to: 5.6
     3Tags: indieweb, microformats, jf2, json-ld, feeds
     4Stable tag: 2.0.0
     5Requires at least: 6.2
     6Requires PHP: 7.4
     7Tested up to: 7.1
    88License: GPLv2 or later
    9 License URI: http://www.gnu.org/licenses/gpl-2.0.html
    10 
    11 Parse This turns URLs into structured jf2 data
    12 
    13 == Description ==
    14 
    15 Parse This is based on a variety of projects including the parsing code from Press This, which was removed from WordPress.
    16 
    17 * It supports parsing from MF2 if present
    18 * For sites that are not marked up with Microformats 2(MF2) it will fall back onto parsing JSON-LD, then HTML/OpenGraph/Dublin Core Tags/etc.
    19 * It supports parsing of JSONFeed and RSS/Atom feeds
    20 * It supports parsing of  WordPress REST API endpoints to generate a site feed
    21 
    22 The goal is to produce structured jf2 data that can be used for previewing links as well as feed readers and other options. It is also bundled in the Post Kinds and Yarns Microsub plugins as a library.
    23 
    24 It can be installed as a standalone plugin which will provide the necessary libraries and functionality as well as the REST API endpoint for getting JF2 data from an arbitrary URL or a WordPress Post.
    25 
     9License URI: https://www.gnu.org/licenses/gpl-2.0.html
     10
     11Parse This turns URLs into structured jf2 data.
     12
     13== Description ==
     14
     15Parse This fetches a URL and turns it into [jf2](https://jf2.spec.indieweb.org/), a simple JSON format for posts, people and feeds. The result can be used for link previews, feed readers, replies and likes, and similar features.
     16
     17It started from the parsing code in Press This, which was removed from WordPress, and has grown from there. It runs as a standalone plugin that other plugins, such as Post Kinds and Yarns Microsub, can require or bundle.
     18
     19It also runs on ClassicPress 2.x.
     20
     21= What it parses =
     22
     23* **Microformats2.** When a page is marked up with microformats, they are used first: entries, cards, events, reviews (including h-entry reviews with `review-of`), recipes, products and any other type. On a busy page it picks the item the page is about, and it follows the IndieWeb authorship algorithm.
     24* **Other metadata.** Other sources only fill in what microformats don't provide: a site-specific parser for YouTube or X (Twitter), then JSON-LD (articles, events, reviews, products, recipes, media and people) and Open Graph, Dublin Core and other meta tags. If there is still no content, the page's WordPress REST API version is fetched, when the site advertises one.
     25* **Parts of pages.** A link with a fragment (`#comment-12`) is parsed as just that part of the page. A WordPress comment link whose page has no microformats for it is read from the site's REST API as a reply.
     26* **Feeds.** RSS and Atom (through WordPress's SimplePie), JSON Feed 1 and 1.1, jf2 and mf2 JSON, and WordPress REST API post collections, including feeds served with the wrong content type.
     27* **Feed discovery.** It lists the feeds a page offers: alternates, `rel=feed` links, h-feeds and the WordPress REST API.
     28
     29On PHP 8.4 and later, HTML is parsed with PHP's own HTML5 parser; on older PHP, with the bundled masterminds/html5 parser.
     30
     31= Using it from PHP =
     32
     33    $parse = new ParseThis\Parser( 'https://example.com/a-post/' );
     34    $result = $parse->fetch();
     35    if ( ! is_wp_error( $result ) ) {
     36        $parse->parse( array( 'return' => 'single' ) );
     37        $jf2 = $parse->get();          // jf2 array.
     38        $mf2 = $parse->get( 'mf2' );   // Or the same result as mf2.
     39    }
     40
     41`ParseThis\Parser::parse()` accepts these arguments:
     42
     43* `return`: `single` for one item (default) or `feed` for a list of items.
     44* `follow`: fetch and parse external author pages. Default false.
     45* `limit`: maximum number of feed items. Default 150.
     46* `jsonld`: try JSON-LD. Default true.
     47* `html`: fall back to meta tags. Default true.
     48* `references`: move nested objects with a URL (citations, cards, events, ...) into `refs`, as the jf2 spec describes. Default true.
     49* `location`: flatten a nested location into `latitude`, `longitude` and `altitude` properties, with `location` as a plain string. Default false.
     50* `alternate`: use a `rel=alternate` jf2 or mf2 version of the page if it has one. Default false.
     51* `require_content`: whether a summary alone isn't enough, so the page's WordPress REST API version is fetched for full content. Default: true for feeds, false otherwise.
     52* `always_arrays`: always return `category`, `photo`, `video`, `audio`, `syndication`, `like-of`, `repost-of`, `bookmark-of` and `in-reply-to` as arrays, as Microsub does. Default false, which follows jf2: a single value is not wrapped in an array.
     53* `debug`: include the raw data each source was read from (`_meta`, `_jsonld`, `_json`, `_yt`, `_ombed`, `_rest`). Default false. Before 2.0.0 this was added whenever `WP_DEBUG` was on.
     54
     55To list a page's feeds instead, use `( new ParseThis\Discovery() )->fetch( $url )`.
     56
     57= Output format =
     58
     59Results follow [jf2](https://jf2.spec.indieweb.org/), with a few deliberate differences:
     60
     61* A feed's entries are in `items`, as in Microsub timelines, rather than the JF2 Feed profile's `children`.
     62* Nested objects with a URL (citations, cards, events, products and any other type) are stored in `refs`, keyed by URL, as XRay does, rather than jf2's `references`. The property holds the URL. Authors stay cards.
     63* Microformats properties that Parse This doesn't know are passed through as they are.
     64* Entries get a `post-type` property from [Post Type Discovery](https://www.w3.org/TR/post-type-discovery/).
     65* Reviews can be an h-review, or an h-entry with `review-of` (a URL, or a nested h-cite, h-card, h-event, h-item or h-product) and `rating`, `best` and `worst`, as proposed in [microformats/h-entry#32](https://github.com/microformats/h-entry/issues/32) and published by Post Kinds. Both, and an h-entry that is also an h-review, get the post type `review`.
     66* `author` is always a card (or a list of cards), never a plain string.
     67* Properties starting with an underscore (`_links`, `_rest`, ...) are internal or debugging data.
     68* A fetched result has `_code`, the HTTP status (or the status the page declares with `<meta http-equiv="Status">`, such as 410 for a deleted post's stub), and `_source_format`, what it was read from: `mf2+html` (microformats), `html` (meta tags only), `mf2+json`, `jf2+json`, `feed+json` (JSON Feed), `xml` (RSS or Atom) or `wordpress` (REST API). After redirects, the document is read from, and relative links resolve against, the address it ended up at.
     69* When a URL can't be parsed, `fetch()` returns a `WP_Error`: `invalid-url`, `not_found` (404), `unauthorized` (401), `forbidden` (403), `http_error` (any other 4xx or 5xx, with the status as `response_code` in the error data), `content-type` (a format Parse This doesn't read), or the request error itself (a timeout, for example).
     70
     71Values from fetched pages are sanitized: URL properties only contain `http` and `https` URLs, plain-text properties (`name`, `summary`, `category`, `content`'s `text`) have no HTML tags, and `content`'s `html` is limited to a safe set of tags. Escape values when you output them all the same, as with any remote data. Debugging data (`_jsonld`, `_meta`, ...) is not sanitized.
     72
     73= REST API =
     74
     75`GET /wp-json/parse-this/1.0/parse?url=https://example.com/`
     76
     77* `url` (required): the URL to parse.
     78* `return`, `follow`, `references`, `location`, `require_content`, `always_arrays`, `debug`: as for `parse()` above.
     79* `mf2`: return mf2 instead of jf2.
     80* `discovery`: list the URL's feeds instead of parsing it.
     81* `nocache`: fetch the URL again instead of using a cached result.
     82
     83Results are cached for 15 minutes per URL and set of parameters, so pasting the same link again doesn't fetch it again. Requests with `debug` are never cached.
     84
     85The endpoint requires the `edit_posts` capability (Contributors and above); the `parse_this_rest_capability` filter changes it. Anyone who can use it can also try it from **Tools > Parse This**.
     86
     87= Filters =
     88
     89* `pt_rewrite_secure`: the list of domains whose `http://` URLs are upgraded to `https://` before fetching.
     90* `parse_this_img_filters`: an image URL found in a page, after the built-in exclusions (ads, spinners, tracking pixels and so on). Return an empty string to drop it.
     91* `parse_this_url_shorteners`: the hosts treated as link shorteners. Links to them in a summary are expanded to where they redirect; other links are left as they are, without a request.
     92* `parse_this_native_html_parser`: whether to parse HTML with PHP's own HTML5 parser on PHP 8.4 and later. Default true. Return false to use the bundled masterminds/html5 parser instead; the two only differ on invalid markup, where PHP's parser does what browsers do.
     93* `parse_this_max_requests`: how many further requests one parse may make for followed author pages and short-link expansion. Default 10. Receives the URL.
     94* `parse_this_cache_lifetime`: how long REST endpoint results are cached, in seconds. Default 15 minutes. Return 0 to turn caching off. Receives the URL.
     95* `parse_this_rest_api_jf2_type`: the jf2 type for a post read through a site's WordPress REST API. Default `entry`. Receives the REST API post object, which includes its WordPress post type, and the site's REST API root URL.
     96
     97= Helper functions =
     98
     99All classes and functions are in the `ParseThis` namespace.
     100
     101* `ParseThis\mf2_to_jf2()` and `ParseThis\jf2_to_mf2()` convert between the two formats.
     102* `ParseThis\post_type_discovery()` returns the IndieWeb post type (note, article, reply, like, photo, ...) of a jf2 or mf2 entry.
     103* `ParseThis\jf2_references()` and `ParseThis\jf2_location()` apply the `references` and `location` transformations to any jf2 object.
     104
     105Every function and class is documented in the source.
     106
     107= Upgrading from 1.x =
     108
     109Version 2.0.0 moved everything into the `ParseThis` namespace and dropped the `Parse_This_` prefix from class names (for example `Parse_This_MF2` is now `ParseThis\MF2`, and `Parse_This` is `ParseThis\Parser`). The old names that Post Kinds and Yarns use still work for now, as deprecated aliases:
     110
     111* Classes: `Parse_This`, `Parse_This_Discovery`, `Parse_This_MF2`, `Parse_This_MF2_Utils` and `REST_Parse_This`.
     112* Functions: `mf2_to_jf2()`, `jf2_to_mf2()`, `post_type_discovery()`, `pt_load_domdocument()` and `seconds_to_iso8601()`.
     113
     114Other old global names are no longer defined. Please switch to the namespaced names.
     115
     116= Using Parse This from another plugin =
     117
     118The recommended way is to require the Parse This plugin rather than bundle a copy. On WordPress 6.5 and later, add `Requires Plugins: parse-this` to your plugin's header. On older WordPress and ClassicPress, which don't read that header, check that Parse This is loaded and new enough before using it:
     119
     120    add_action( 'plugins_loaded', function () {
     121        if ( ! defined( 'PARSE_THIS_VERSION' ) || version_compare( PARSE_THIS_VERSION, '2.0.0', '<' ) ) {
     122            // Show a notice asking for Parse This 2.0.0 or later, and skip the features that need it.
     123            return;
     124        }
     125        // Use ParseThis\Parser and the rest here.
     126    }, 11 );
     127
     128`PARSE_THIS_VERSION` is the version of the copy of Parse This that loaded. It isn't defined by copies older than 2.0.0, so an older copy bundled in another plugin shows up as missing. The `parse_this_loaded` action fires, with the version, once Parse This has loaded.
     129
     130= Bundling Parse This in another plugin =
     131
     132If you do bundle it, copy the plugin into your plugin (for example under `lib/parse-this/`) and load it only if the standalone plugin hasn't already, from `plugins_loaded` at priority 10 or later:
     133
     134    add_action( 'plugins_loaded', function () {
     135        if ( ! function_exists( 'parse_this_loader' ) ) {
     136            require_once __DIR__ . '/lib/parse-this/parse-this.php';
     137            parse_this_loader();
     138        }
     139    }, 11 );
     140
     141The standalone plugin loads at priority 9, so when it is active it is used instead of your copy. Every function is also wrapped in `function_exists()` and classes are loaded by an autoloader, so if two copies do load, the first one wins.
     142
     143Copies of Parse This 1.x bundled in other plugins keep working alongside 2.0: when the standalone plugin is active, their calls to the old names listed under "Upgrading from 1.x" are served by the standalone plugin.
    26144
    27145== Frequently Asked Questions ==
    28146
     147= What is jf2? =
     148
     149jf2 is a JSON format derived from microformats2, but simpler: single values aren't wrapped in arrays, and types drop the `h-` prefix. See the [jf2 specification](https://jf2.spec.indieweb.org/).
     150
     151= Why does a page return only a title and a URL? =
     152
     153That page has no microformats, JSON-LD or useful meta tags, or it blocks automated requests. The **Tools > Parse This** page shows exactly what was parsed for a URL.
     154
     155= Which copy is used when several plugins bundle Parse This? =
     156
     157The first one loaded. If the standalone plugin is active, it is that one. See "Bundling Parse This in another plugin" above.
     158
     159= Does it work with ClassicPress? =
     160
     161Yes. It is tested with ClassicPress 2.7 on PHP 7.4 to 8.3.
     162
    29163== Changelog ==
     164
     165= 2.0.0 ( 2026-10-03 ) =
     166
     167A major update. Please read "Upgrading from 1.x" if another plugin of yours uses Parse This.
     168
     169Requirements:
     170
     171* Requires PHP 7.4 and WordPress 6.2, or ClassicPress 2.x. Tested up to WordPress 7.1 and PHP 8.5.
     172
     173Changes to be aware of:
     174
     175* Everything is in the `ParseThis` namespace, and class names drop the `Parse_This_` prefix (`Parse_This` is now `ParseThis\Parser`). The old names Post Kinds and Yarns use still work, as deprecated aliases.
     176* Output: nested objects with a URL move to `refs`; authors are always cards; RSS and Atom items use `post-type` (not `post_type`); unknown microformats properties are kept; and fetched results carry `_code` and `_source_format`.
     177* `fetch()` returns a `WP_Error` for HTTP error pages (`not_found`, `unauthorized`, `forbidden`, `http_error`) instead of parsing them; a 410 Gone page is still parsed.
     178* The REST endpoint and the Tools > Parse This page require the `edit_posts` capability (filterable with `parse_this_rest_capability`), and its parameters are validated.
     179* Raw source data (`_meta`, `_jsonld` and so on) is only included with the new `debug` argument, not whenever `WP_DEBUG` is on.
     180* Plugins that use Parse This should require it rather than bundle it: see "Using Parse This from another plugin". `PARSE_THIS_VERSION` and the `parse_this_loaded` action help with that.
     181* The Instagram parser and the `ifset()` helper are removed.
     182
     183New:
     184
     185* Microformats always come first; JSON-LD, meta tags and the REST API only fill gaps. The `require_content` argument controls when a summary is enough.
     186* Microformats: choose the item a busy page is about; the full authorship algorithm, with feed entries inheriting the feed's author; reviews as an h-entry with `review-of`, `rating`, `best` and `worst` (as Post Kinds publishes them); complete h-event, h-review and h-recipe parsing; `follow-of`; and every value of repeated properties.
     187* Links with a fragment parse just that element; WordPress comment links fall back to the site's REST API.
     188* JSON-LD: reviews, products, recipes, and Article and Event subtypes, preferred over the WebPage node SEO plugins add.
     189* Feeds and JSON served with the wrong content type are recognized. JSON Feed items get plain text, a `post-type` and absolute URLs.
     190* Discovery finds mf2 JSON alternates, `rel=feed` links, RSS 1.0, multi-value `rel` attributes and JSON Feeds advertised as `application/json`, and follows permanent redirects.
     191* PHP's own HTML5 parser is used on PHP 8.4 and later: several times faster, and it handles invalid markup as browsers do (`parse_this_native_html_parser` turns it off).
     192* The `always_arrays` argument returns Microsub-style arrays.
     193* New filters: `parse_this_rest_api_jf2_type`, `parse_this_url_shorteners`, `parse_this_max_requests`, `parse_this_cache_lifetime`, `parse_this_rest_capability` and `parse_this_native_html_parser`.
     194
     195Fixes:
     196
     197* More than 50 bugs, many of them fatal errors on PHP 8, across RSS and Atom, the WordPress REST API, JSON-LD, meta tags, microformats, JSON Feed, X (Twitter), YouTube and feed discovery.
     198* Content HTML no longer loses the text before its first tag, and escaped text (`&lt;code&gt;`) no longer turns into markup.
     199* REST API posts are dated from their GMT dates; YouTube pages are read in full; titles that only repeat the content are dropped; names and ratings of "0" are kept; post types follow Post Type Discovery.
     200
     201Performance:
     202
     203* Feeds are downloaded once rather than twice; only link-shortener URLs in summaries are expanded; REST API tags and site details need far fewer requests; followed author pages are fetched once per request; REST endpoint results are cached for 15 minutes (`nocache` and `parse_this_cache_lifetime`).
     204
     205Security:
     206
     207* Output from fetched pages is sanitized (URLs limited to http and https, tags stripped from plain text, a safe set of HTML tags in content); one parse makes at most 10 further requests; the debug page sends its nonce in a header; plugin files exit when loaded outside WordPress; the OPML class handles invalid input safely.
     208
     209Developers:
     210
     211* Core's `fetch_feed()` is used for RSS and Atom; all requests go through `ParseThis\pt_remote_get()`.
     212* The bundled php-mf2 (0.5.0) and masterminds/html5 (2.11.0) are updated, and polyfills WordPress 6.2 provides are removed.
     213* Every function, class and hook is documented, and the plugin is tested on WordPress 6.2, the latest WordPress and ClassicPress 2.7 with PHP 7.4 to 8.5.
    30214
    31215= 1.0.1 ( 2021-04-02 ) =
    … …  
    35219= 1.0.0 ( 2020-12-15 ) =
    36220* First Official Release. Prior to this point it was in a point release.
     221
     222== Upgrade Notice ==
     223
     224= 2.0.0 =
     225A major update: classes and functions move to the ParseThis namespace (the old names Post Kinds and Yarns use still work), the output format changes in places, and the REST endpoint now requires the edit_posts capability. Requires PHP 7.4 and WordPress 6.2.
  • indieweb-post-kinds/trunk/readme.txt

    r3067251 r3738687  
    22Contributors: dshanske
    33Tags: indieweb, interaction, posts, webmention, share, like, scrobble
    4 Stable tag: 3.7.3
    5 Requires at least: 4.9.9
    6 Requires PHP: 7.0
    7 Tested up to: 6.5
     4Stable tag: 4.0.0
     5Requires at least: 6.2
     6Requires PHP: 7.4
     7Tested up to: 7.1
    88License: GPLv2 or later
    99License URI: http://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2121Many sites will not need all of the kinds set up. What kinds of posts you wish to publish are specific to your needs.
    2222
     23Post Kinds requires PHP 7.4 or later and WordPress 6.2 or later. It also supports ClassicPress 2.7.3 or later.
     24
    2325
    2426== Screenshots ==
    … …  
    4244
    4345== Upgrade Notice ==
     46
     47= 4.0.0 =
     48
     49Requires PHP 7.4 and WordPress 6.2 or ClassicPress 2.7.3. Response data is now escaped, changing some display helpers; stored post data upgrades in the background. Posts from apps and the REST API now get the Default Kind (Note unless changed), not Article. See the changelog.
    4450
    4551= 3.7.0 =
    … …  
    118124either a response to something, such as a URL, or a more passive type of post where you are recording/logging something you did, for
    119125example, watched a movie.
     126
     127When you write a post in the Classic Editor, choose its kind in the Kind box. For kinds that respond to something, fill in the Response Properties box:
     128
     129 * **URL** - the page you are responding to. When you leave this field, the plugin fetches the page and fills in the other fields from it where it can. You can edit or clear anything it fills in.
     130 * **Name** - the title of what you are responding to.
     131 * **RSVP** and **Rating** - shown only for the kinds that use them: RSVP for RSVPs, and a rating for reviews, listens, watches, reads, plays and similar.
     132 * **Details** - more about what you are responding to: a summary, quote or caption (plain text), the site name, publication or album, when it was published and updated, its tags (separated by semicolons) and a featured image URL. For listens, watches, photos and similar, it also has the duration, or a start and end time.
     133 * **Author** - the name, URL and photo of the author or artist. Separate several authors with semicolons.
     134 * **Upload or Attach Media** - for photo, video and audio posts, attaches a file from the media library.
     135 * **Clear** - empties every field.
     136
     137The plugin uses these fields to display the response above your post's content, marked up with [microformats](https://microformats.org/wiki/h-entry) so other sites can read it.
    120138
    121139= How do I interact with other sites? =
    … …  
    203221However if you are interested in creating your own there is functionality around it.
    204222
    205 register_post_kind(
    206         'reply',
     223        register_post_kind(
     224                'reply',
    207225                array(
    208                         'singular_name'   => __( 'Reply', 'indieweb-post-kinds' ), // Name for one instance of the kind
    209                         'name'            => __( 'Replies', 'indieweb-post-kinds' ), // General name for the kind plural
    210                         'verb'            => __( 'Replied to', 'indieweb-post-kinds' ), // The string for the verb or action (liked this)
    211                         'property'        => 'in-reply-to', // microformats 2 property
    212                         'format'          => 'link', // Post Format that maps to this
    213                         'description'     => __( 'a reply to content typically on another site', 'indieweb-post-kinds' ),
    214                         'description-url' => 'http://indieweb.org/reply',
    215                         'title'           => false, // Should this kind have an explicit title
    216                         'show'            => true, // Show in Settings
    217                         )
     226                        'singular_name'   => __( 'Reply', 'indieweb-post-kinds' ), // Name for one instance of the kind.
     227                        'name'            => __( 'Replies', 'indieweb-post-kinds' ), // General name for the kind, plural.
     228                        'verb'            => __( 'Replied to', 'indieweb-post-kinds' ), // The verb or action, such as "Liked".
     229                        'property'        => 'in-reply-to', // Microformats 2 property.
     230                        'format'          => 'link', // Post format that maps to this kind.
     231                        'description'     => __( 'a reply to content typically on another site', 'indieweb-post-kinds' ),
     232                        'description_url' => 'http://indieweb.org/reply',
     233                        'title'           => false, // Whether this kind should have an explicit title.
     234                        'show'            => true, // Whether to show the kind in Settings.
    218235                )
    219 );
    220 
    221 Add a function with your kind in the above format, hooking it in the init hook and it will add the Kind to the system.
     236        );
     237
     238Call it from a function hooked to `init` and it will add the Kind to the system. The [developer notes](https://github.com/dshanske/indieweb-post-kinds/blob/trunk/develop.txt) list the other arguments.
    222239
    223240= Can I enable one of the Kinds you plan to offer in future? =
    … …  
    225242`set_post_kind_visibility( $slug, $show = true )` - If you add this function in early on, it will change the visibility of a kind.
    226243
    227 = Can I create archives for each kind? ==
     244= Can I create archives for each kind? =
    228245
    229246Post Kinds automatically handles the display of archives of individual types. So to view all the posts marked as "note", for example, one could visit the URL http://www.YOURSITE.COM/kind/note/.
    230247Simply replace YOURSITE.COM with your particular site name and the particular post kind name to access the others.
    231248
     249To use another word than "kind" in these URLs, for example in your own language, set the Kind base on Settings > Permalinks. Links to the old URLs then stop working, as when the category base changes.
     250
    232251You can also add the date /kind/note/2018/12/24 to see date-based archives.
    233252
    234 For archives if you add exclude_kind as a query variable it will exclude specific kinds from the query `?exclude=kind&exclude_terms=note`. You can also do this as /exclude/kind/note,checkin as it accepts multiple values
    235 
    236 There is also a special photo photo included, using ?kind_photos=1 or /photos or /photos/yyyy or /photos/yyyy/mm or /photos/kind/note or any other taxonomy. This will use the photo enhancements introduced in 3.4.0 to only show photos from
     253To leave specific kinds out of an archive, use `?exclude=kind&exclude_terms=note` or /exclude/kind/note,checkin, which accepts several kinds.
     254
     255There is also a special photo archive, using ?kind_photos=1 or /photos or /photos/yyyy or /photos/yyyy/mm or /photos/kind/note or any other taxonomy. This will use the photo enhancements introduced in 3.4.0 to only show photos from
    237256all types of posts.
    238257
    … …  
    270289== Theme Support ==
    271290
    272 Post Kinds automatically adds information to `the_content` and `the_excerpt` filter. Being as this is inside the content block, which may or may not be desirable, you may remove these filters as noted
    273 below and call `kind_display` directly. This will allow it to appear outside the content block. To remove the automatic display, add the following to your theme.
    274         * `add_filter( 'kind_content_display', '__return_false' );`
    275 
    276 The functions `has_post_kind`, `set_post_kind`, and `set_post_kind` will allow you to manipulate the kind settings in a post. `get_post_kind_string` will return the display name of a kind.
    277 
    278 If you want to customize the look of the display, you can create a directory in your theme called `kind_views`, copy the file from the views directory of the plugin, and modify it. This will persist
    279 through future plugin updates.
     291Post Kinds works with most themes without changes. It adds the response (what you replied to, liked, listened to and so on) to the post's content and excerpt, and to feeds.
     292
     293To place the response somewhere else in your theme, turn off the automatic display and call `kind_display()` in your template where you want it:
     294
     295        add_filter( 'kind_content_display', '__return_false' );
     296
     297Many kinds have no title. In templates that list posts, `kind_get_the_title()` returns the post's title, or generates one from the kind and what it responds to.
     298
     299To change how a kind looks, create a `kind_views` folder in your theme, copy the view from the plugin's `views` folder into it, and edit the copy. It persists through plugin updates, but it doesn't receive fixes: compare it with the plugin's views after upgrading.
     300
     301To style responses, target the `.response` wrapper that each view uses, and `.kind-embed` around embedded content.
     302
     303The template functions, hooks and view variables are listed in the [developer notes](https://github.com/dshanske/indieweb-post-kinds/blob/trunk/develop.txt).
    280304
    281305
    282306== Changelog ==
    283307
    284 * 3.7.3 ( 2024-04-09 ) =
     308= 4.0.0 ( 2026-10-10 ) =
     309* Raise the minimum requirements to PHP 7.4 and WordPress 6.2, and declare ClassicPress 2.7.3 support
     310* The `kind` field in the REST API now has a description and lists the valid kinds; an unknown kind is rejected with `rest_invalid_param`
     311* When the old IndieWeb Taxonomy plugin is active, activation now stops with a translatable message and deactivates Post Kinds itself, instead of a file that isn't a plugin
     312* The Default Kind setting applies to every new post without a kind, including posts from apps, the REST API, Quick Draft and scheduled drafts, which used to get Article. Set Default Kind to Article to keep the old behaviour. A translated site no longer gets a stray copy of the Article kind
     313* Escape response data in views, the editor, widgets and settings; summaries are displayed as plain text
     314* Sanitize input from the editor, the media modal's artist fields, settings and widgets; citation summaries are saved as plain text
     315* An RSVP can now be cleared in the editor
     316* Fix the Response Properties box disappearing for every kind after another box was collapsed while editing a note or article; it can no longer be hidden through Screen Options, since the editor shows it only for kinds that use it
     317* Kind icons can be resized from a theme with any rule, such as `.svg-icon { width: 1rem; height: 1rem; }`. The inline style is gone, the plugin's styles have no specificity, and icons have a default size where the stylesheet doesn't load, such as feed readers
     318* Deleting the plugin removes its settings and scheduled events, on every site of a multisite network. Posts, their kinds and their response data are kept
     319* Several authors are saved as one h-card each, pairing the semicolon-separated names, URLs and photos by position, and every author is displayed. Lists of authors from Micropub or link previews are shown and filled in too. Authors saved by earlier versions are read the same way. An author with a URL but no photo is linked, and clearing the author fields removes the authors
     320* Add a Kind base setting to Settings > Permalinks, to change the `kind` in kind archive URLs such as /kind/like/, and the `kind_base_slug` filter
     321* Fix durations calculated from the start and end times never being shown; `calculate_duration()` now also accepts date objects
     322* Require the edit_posts capability for the post-kinds/1.0/fields REST route; an unknown kind returns a 404
     323* Add a background upgrade that moves and repairs stored citations and records the media in each post; `wp post-kinds upgrade` runs it on demand (`--dry-run`, `--post=<id>`)
     324* Fix citations losing their type when viewed (a checkin lost its h-card, for example) and nesting inside themselves when saved again; the upgrade repairs existing posts
     325* Fix old citations stored as a plain URL being lost when viewed, and a PHP 8 fatal error when MF2_Post read very old response data
     326* Reading a post no longer writes to the database or downloads remote photos
     327* Performance: the media scan on save covers posts only and reads the content once; attachment lookups are cached; the photos archive and the home page kind filter use simpler queries; the settings page only writes kind terms that changed; icons, time zone offsets and permalink kinds are cached; the media fragment script loads only where there is audio or video, deferred
     328* The Kind Post widget prints nothing when it has no posts, instead of an unclosed wrapper
     329* New kind terms are named after the kind rather than its slug
     330* Feed change: The RSS and Atom feeds use WordPress's own templates instead of copies, and other plugins' feed handlers are no longer removed. Posts without a title have an empty title element instead of none, so the Atom feed is valid; turn on "Automatically add the Kind to the Title" to fill it in. Dates are written in UTC. In comment feeds, comments on untitled posts name the post.
     331* Deprecated: The global functions `display_formatted_datetime()`, `divide_datetime()`, `build_datetime()`, `get_datetime_offset()`, `divide_interval()` and `build_interval()`. Use the `Kind_Time` methods of the same names. The global names still work but report a deprecation notice when debugging is on, and they will be removed in the next major release (5.0.0). Custom views copied into a theme may call them, so update those to the `Kind_Time` methods.
     332* Display change: Custom views copied into a theme's `kind_views` folder keep their old code. Copy them again from the plugin's `views` folder, or escape their output.
     333* Display change: Citation summaries are plain text, marked up as `p-summary` instead of `e-summary`. `Kind_View::get_summary()` returns the summary markup.
     334* Display change: `Kind_View::get_hcard()`, `get_cite_title()`, `get_site_name()`, `get_embed()`, `get_url_link()` and `display_duration()` return escaped markup; HTML passed as a name is shown as text.
     335* Display change: `kind_get_the_title()` escapes the content before the `kind_get_the_title_content` filter.
     336* Display change: `Kind_View::rsvp_text()` takes optional URL and name arguments and returns the full sentence. Called with only a type, it still returns a printf-ready string. The RSVP strings have changed, so translations need updating.
     337* Display change: `Kind_View::rating_text()` ignores non-numeric ratings and shows at most 10 stars.
     338* Upgrade change: Published posts with no kind are given one, based on their stored response properties, or Article. If the home page shows only selected kinds, Article is added to them so those posts stay; posts given another kind, such as Like, follow the selection.
     339* Upgrade change: Until the background upgrade reaches them, older posts may be missing from the photos archive; `wp post-kinds upgrade` finishes it at once.
     340* Removed: The `kind_photo_shortcode_exclude` filter, which never ran.
     341* Behaviour change: `MF2_Post::get_images()` no longer imports remote photos into the media library; it returns media library attachments only.
     342* Deprecated: The `MF2_Post` class. Use `Kind_Post`. Creating an `MF2_Post` reports a deprecation notice when debugging is on; the class will be removed in a future major version.
     343
     344= 3.7.3 ( 2024-04-09 ) =
    285345* Ensure widget array is instantiated
    286346
  • indieweb-post-kinds/trunk/templates/reply-author.php

    r2399112 r3738687  
     1<?php
     2/**
     3 * Author tab of the Post Kinds metabox.
     4 *
     5 * Included by reply-metabox.php.
     6 *
     7 * @package Post_Kinds
     8 * @since   3.0.0
     9 *
     10 * @var array $cite The citation, normalized by Kind_Post::normalize_cite().
     11 */
     12
     13defined( 'ABSPATH' ) || exit;
     14?>
    115        <div id="kind-author" class="hide-if-js">
    2         <h4> <?php _e( 'Information on the Author or Artist of the Piece', 'indieweb-post-kinds' ); ?></h4>
    3                 <?php _e( '(Multiple Entries separated by semicolon)', 'indieweb-post-kinds' ); ?><BR />
     16        <h4> <?php esc_html_e( 'Information on the Author or Artist of the Piece', 'indieweb-post-kinds' ); ?></h4>
     17                <?php esc_html_e( '(Multiple Entries separated by semicolon)', 'indieweb-post-kinds' ); ?><BR />
    418        <p class="field-row">
    519        <label for="cite_author_name" class="three-quarters">
    6                 <?php _e( 'Author', 'indieweb-post-kinds' ); ?>
    7                         <input type="text" name="cite_author_name" id="cite_author_name" class="widefat" value="<?php echo $cite['author']['name']; ?>" />
     20                <?php esc_html_e( 'Author', 'indieweb-post-kinds' ); ?>
     21                        <input type="text" name="cite_author_name" id="cite_author_name" class="widefat" value="<?php echo esc_attr( $cite['author']['name'] ); ?>" />
    822        </label>
    923        </p>
    1024        <p class="field-row">
    1125        <label for="cite_author_url" class="three-quarters">
    12                 <?php _e( 'Author URL', 'indieweb-post-kinds' ); ?>
    13                         <input type="text" name="cite_author_url" id="cite_author_url" class="widefat" value="<?php echo $cite['author']['url']; ?>" />
     26                <?php esc_html_e( 'Author URL', 'indieweb-post-kinds' ); ?>
     27                        <input type="text" name="cite_author_url" id="cite_author_url" class="widefat" value="<?php echo esc_attr( $cite['author']['url'] ); ?>" />
    1428        </label>
    1529        </p>
    1630        <p class="field-row">
    1731        <label for="cite_author_photo" class="three-quarters">
    18                 <?php _e( 'Author Photo URL', 'indieweb-post-kinds' ); ?>
    19                         <input type="text" name="cite_author_photo" id="cite_author_photo" class="widefat" value="<?php echo $cite['author']['photo']; ?>" />
     32                <?php esc_html_e( 'Author Photo URL', 'indieweb-post-kinds' ); ?>
     33                        <input type="text" name="cite_author_photo" id="cite_author_photo" class="widefat" value="<?php echo esc_attr( $cite['author']['photo'] ); ?>" />
    2034        </label>
    2135        </p>
  • indieweb-post-kinds/trunk/templates/reply-details.php

    r2399112 r3738687  
     1<?php
     2/**
     3 * Details tab of the Post Kinds metabox.
     4 *
     5 * Included by reply-metabox.php; includes reply-time.php.
     6 *
     7 * @package Post_Kinds
     8 * @since   3.0.0
     9 *
     10 * @var Kind_Post $kind_post The post being edited.
     11 * @var array     $cite      The citation, normalized by Kind_Post::normalize_cite().
     12 */
     13
     14defined( 'ABSPATH' ) || exit;
     15?>
    116<div id="kind-details" class="hide-if-js">
    2         <h4><?php _e( 'Information on what you are responding to', 'indieweb-post-kinds' ); ?></h4>
     17                <h4><?php esc_html_e( 'Information on what you are responding to', 'indieweb-post-kinds' ); ?></h4>
    318        <label for="cite_summary">
    4                 <?php _e( 'Summary/Quote/Caption', 'indieweb-post-kinds' ); ?>
    5         <textarea name="cite_summary" id="cite_summary" data-role="none" class="widefat"><?php echo $cite['summary']; ?></textarea>
     19                <?php esc_html_e( 'Summary/Quote/Caption', 'indieweb-post-kinds' ); ?>
     20        <textarea name="cite_summary" id="cite_summary" data-role="none" class="widefat"><?php echo esc_textarea( html_entity_decode( wp_strip_all_tags( (string) $cite['summary'] ), ENT_QUOTES, get_bloginfo( 'charset' ) ) ); ?></textarea>
    621        </label>
    722        <p class="field-row">
    823        <label for="cite_publication" class="three-quarters">
    9                 <?php _e( 'Site Name/Publication/Album', 'indieweb-post-kinds' ); ?>
    10                         <input type="text" name="cite_publication" id="cite_publication" class="widefat" value="<?php echo $cite['publication']; ?>" />
     24                <?php esc_html_e( 'Site Name/Publication/Album', 'indieweb-post-kinds' ); ?>
     25                        <input type="text" name="cite_publication" id="cite_publication" class="widefat" value="<?php echo esc_attr( $cite['publication'] ); ?>" />
    1126        </label>
    1227        </p>
    1328        <p class="field-row">
    14                         <?php echo Kind_Metabox::kind_the_time( 'cite_published', __( 'Published/Released', 'indieweb-post-kinds' ), $cite['published'], 'published' ); ?>
    15                 <?php echo Kind_Metabox::kind_the_time( 'cite_updated', __( 'Updated', 'indieweb-post-kinds' ), $cite['updated'], 'updated' ); ?>
     29                        <?php echo Kind_Metabox::kind_the_time( 'cite_published', __( 'Published/Released', 'indieweb-post-kinds' ), $cite['published'], 'published' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::kind_the_time() escapes its output. ?>
     30                <?php echo Kind_Metabox::kind_the_time( 'cite_updated', __( 'Updated', 'indieweb-post-kinds' ), $cite['updated'], 'updated' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::kind_the_time() escapes its output. ?>
    1631        </p>
    1732        <label for="cite_tags">
    18                 <?php _e( 'Tags (semicolon separated)', 'indieweb-post-kinds' ); ?>
    19         <textarea name="cite_tags" id="cite_tags" data-role="none" class="widefat"><?php echo $cite['category'] ?></textarea>
     33                <?php esc_html_e( 'Tags (semicolon separated)', 'indieweb-post-kinds' ); ?>
     34        <textarea name="cite_tags" id="cite_tags" data-role="none" class="widefat"><?php echo esc_textarea( $cite['category'] ); ?></textarea>
    2035        </label>
    2136        <p class="field-row">
    2237        <label for="cite_featured" class="three-quarters">
    23                 <?php _e( 'Featured Image', 'indieweb-post-kinds' ); ?>
    24                         <input type="text" name="cite_featured" id="cite_featured" class="widefat" value="<?php echo $cite['featured']; ?>" />
     38                <?php esc_html_e( 'Featured Image', 'indieweb-post-kinds' ); ?>
     39                        <input type="text" name="cite_featured" id="cite_featured" class="widefat" value="<?php echo esc_attr( $cite['featured'] ); ?>" />
    2540        </label>
    2641        </p>
  • indieweb-post-kinds/trunk/templates/reply-metabox.php

    r3001401 r3738687  
    11<?php
     2/**
     3 * Post Kinds metabox for the Classic Editor.
     4 *
     5 * Loaded by Kind_Metabox::reply_metabox(). It sets up $kind_post, $kind, $type
     6 * and $cite, then includes reply-details.php and reply-author.php, which use them.
     7 *
     8 * @package Post_Kinds
     9 * @since   3.0.0
     10 */
     11
     12defined( 'ABSPATH' ) || exit;
     13
    214wp_nonce_field( 'replykind_metabox', 'replykind_metabox_nonce' );
    315$kind_post = new Kind_Post( get_the_ID() );
    … …  
    1022}
    1123
    12 if ( empty( $cite['url'] ) && array_key_exists( 'kindurl', $_GET ) ) {
    13         $cite['url'] = esc_url_raw( $_GET['kindurl'] );
     24// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Prefills the URL on the new post screen; nothing is saved.
     25if ( empty( $cite['url'] ) && isset( $_GET['kindurl'] ) && is_string( $_GET['kindurl'] ) ) {
     26        $cite['url'] = sanitize_url( wp_unslash( $_GET['kindurl'] ) );
    1427}
     28// phpcs:enable WordPress.Security.NonceVerification.Recommended
    1529
    1630$attachment = 0;
    … …  
    1832
    1933if ( in_array( $kind, array( 'audio', 'video', 'photo' ) ) ) {
    20         $attachment = attachment_url_to_postid( $cite['url'] );
     34        $attachment = empty( $cite['url'] ) ? 0 : Kind_Post::get_attachment_id( $cite['url'] );
    2135        if ( $attachment ) {
    2236                $attachment_post = new Kind_Post( $attachment );
    … …  
    2842
    2943?>
    30 <a href="#kind-details" class="show-kind-details button hide-if-no-js"><?php _e( 'Details', 'indieweb-post-kinds' ); ?></a>
    31 <a href="#kind-author" class="show-kind-author-details button hide-if-no-js"><?php _e( 'Author', 'indieweb-post-kinds' ); ?></a>
    32 <a id="add-kind-media" class="button hide-if-no-js hidden" href="javascript:;">Upload or Attach Media</a>
    33 <button class="clear-kindmeta-button button hide-if-no-js"><?php _e( 'Clear', 'indieweb-post-kinds' ); ?></button>
     44<a href="#kind-details" class="show-kind-details button hide-if-no-js"><?php esc_html_e( 'Details', 'indieweb-post-kinds' ); ?></a>
     45<a href="#kind-author" class="show-kind-author-details button hide-if-no-js"><?php esc_html_e( 'Author', 'indieweb-post-kinds' ); ?></a>
     46<a id="add-kind-media" class="button hide-if-no-js hidden" href="javascript:;"><?php esc_html_e( 'Upload or Attach Media', 'indieweb-post-kinds' ); ?></a>
     47<button class="clear-kindmeta-button button hide-if-no-js"><?php esc_html_e( 'Clear', 'indieweb-post-kinds' ); ?></button>
    3448<p class="field-row">
    3549        <label for="cite_url" class="three-quarters">
    36                 <?php _e( 'URL', 'indieweb-post-kinds' ); ?>
    37                         <input type="text" name="cite_url" id="cite_url" class="widefat" value="<?php echo $cite['url']; ?>" />
     50                <?php esc_html_e( 'URL', 'indieweb-post-kinds' ); ?>
     51                        <input type="text" name="cite_url" id="cite_url" class="widefat" value="<?php echo esc_attr( $cite['url'] ); ?>" />
    3852        </label>
    3953</p>
    4054<p class="field-row">
    4155        <label for="cite_name" class="three-quarters">
    42                 <?php _e( 'Name', 'indieweb-post-kinds' ); ?>
    43                         <input type="text" name="cite_name" id="cite_name" class="widefat" value="<?php echo $cite['name']; ?>" />
     56                <?php esc_html_e( 'Name', 'indieweb-post-kinds' ); ?>
     57                        <input type="text" name="cite_name" id="cite_name" class="widefat" value="<?php echo esc_attr( $cite['name'] ); ?>" />
    4458        </label>
    4559</p>
    4660<p class="field-row hide-if-js" id="rsvp-option">
    47         <?php echo Kind_Metabox::rsvp_select( $kind_post->get( 'rsvp' ) ); ?>
     61        <?php echo Kind_Metabox::rsvp_select( $kind_post->get( 'rsvp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::rsvp_select() escapes its output. ?>
    4862</p>
    4963<p class="field-row hide-if-js" id="rating-option">
    50         <?php echo Kind_Metabox::rating_select( $kind_post->get( 'rating' ) ); ?>
     64        <?php echo Kind_Metabox::rating_select( $kind_post->get( 'rating' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::rating_select() escapes its output. ?>
    5165</p>
    5266<p id="kind-media hide-if-no-js">
    … …  
    5973        } elseif ( wp_attachment_is( 'audio', $attachment ) ) {
    6074                $view = new Kind_Media_View( $attachment, 'audio' );
    61                 echo $view->get();
     75                echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    6276        } elseif ( wp_attachment_is( 'video', $attachment ) ) {
    6377                $view = new Kind_Media_View( $attachment, 'video' );
    64                 echo $view->get();
     78                echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    6579        }
    6680}
    6781?>
    6882        </div>
    69         <input type="hidden" id="cite_media" name="cite_media" value="<?php echo $attachment; ?>" >
     83        <input type="hidden" id="cite_media" name="cite_media" value="<?php echo absint( $attachment ); ?>" >
    7084</p>
    7185
  • indieweb-post-kinds/trunk/templates/reply-time.php

    r2399112 r3738687  
    1 <?php $duration = divide_interval( $kind_post->get_duration() ); ?>
     1<?php
     2/**
     3 * Duration fields of the Post Kinds metabox.
     4 *
     5 * Included by reply-details.php.
     6 *
     7 * @package Post_Kinds
     8 * @since   3.0.0
     9 *
     10 * @var Kind_Post $kind_post The post being edited.
     11 */
     12
     13defined( 'ABSPATH' ) || exit;
     14
     15$duration = Kind_Time::divide_interval( $kind_post->get_duration() ); ?>
    216
    317<div id="kind-time" class="hide-if-js">
    4         <h4><?php _e( 'Duration', 'indieweb-post-kinds' ); ?></h4>
     18        <h4><?php esc_html_e( 'Duration', 'indieweb-post-kinds' ); ?></h4>
    519        <p class="field-row">
    620        <label for="duration" class="eighth">
    7                 <?php _e( 'Years', 'indieweb-post-kinds' ); ?>
    8                         <input type="number" name="duration_years" id="duration_years" size="3" value="<?php echo $duration['Y']; ?>" />
     21                <?php esc_html_e( 'Years', 'indieweb-post-kinds' ); ?>
     22                        <input type="number" name="duration_years" id="duration_years" size="3" value="<?php echo absint( $duration['Y'] ); ?>" />
    923        </label>
    1024        <label for="duration" class="eighth">
    11                 <?php _e( 'Months', 'indieweb-post-kinds' ); ?>
    12                         <input type="number" name="duration_months" id="duration_months" size="2" min="0" max="12" value="<?php echo $duration['M']; ?>" />
     25                <?php esc_html_e( 'Months', 'indieweb-post-kinds' ); ?>
     26                        <input type="number" name="duration_months" id="duration_months" size="2" min="0" max="12" value="<?php echo absint( $duration['M'] ); ?>" />
    1327        </label>
    1428        <label for="duration" class="eighth">
    15                 <?php _e( 'Days', 'indieweb-post-kinds' ); ?>
    16                         <input type="number" name="duration_days" id="duration_days" size="2" min="0" max="30" value="<?php echo $duration['D']; ?>" />
     29                <?php esc_html_e( 'Days', 'indieweb-post-kinds' ); ?>
     30                        <input type="number" name="duration_days" id="duration_days" size="2" min="0" max="30" value="<?php echo absint( $duration['D'] ); ?>" />
    1731        </label>
    1832        <p>
    1933        <p class="field-row">
    2034        <label for="duration" class="eighth">
    21                 <?php _e( 'Hours', 'indieweb-post-kinds' ); ?>
    22                         <input type="number" name="duration_hours" id="duration_hours" size="2" min="0" max="12" value="<?php echo $duration['H']; ?>" />
     35                <?php esc_html_e( 'Hours', 'indieweb-post-kinds' ); ?>
     36                        <input type="number" name="duration_hours" id="duration_hours" size="2" min="0" max="12" value="<?php echo absint( $duration['H'] ); ?>" />
    2337        </label>
    2438        <label for="duration" class="eighth">
    25                 <?php _e( 'Minutes', 'indieweb-post-kinds' ); ?>
    26                         <input type="number" name="duration_minutes" id="duration_minutes" size="2" min="0" max="60" value="<?php echo $duration['I']; ?>" />
     39                <?php esc_html_e( 'Minutes', 'indieweb-post-kinds' ); ?>
     40                        <input type="number" name="duration_minutes" id="duration_minutes" size="2" min="0" max="60" value="<?php echo absint( $duration['I'] ); ?>" />
    2741        </label>
    2842        <label for="duration" class="eighth">
    29                 <?php _e( 'Seconds', 'indieweb-post-kinds' ); ?>
    30                         <input type="number" name="duration_seconds" id="duration_seconds" size="2" min="0" max="60" value="<?php echo $duration['S']; ?>" />
     43                <?php esc_html_e( 'Seconds', 'indieweb-post-kinds' ); ?>
     44                        <input type="number" name="duration_seconds" id="duration_seconds" size="2" min="0" max="60" value="<?php echo absint( $duration['S'] ); ?>" />
    3145        </label>
    3246        </p>
    33         <h4> <?php _e( 'Start Time and End Time will be Used to Calculate Duration if Duration Not Set.', 'indieweb-post-kinds' ); ?> </h4>
     47        <h4> <?php esc_html_e( 'Start Time and End Time will be Used to Calculate Duration if Duration Not Set.', 'indieweb-post-kinds' ); ?> </h4>
    3448        <p class="field-row">
    35                 <?php echo Kind_Metabox::kind_the_time( 'mf2_start', __( 'Start Time', 'indieweb-post-kinds' ), $kind_post->get_datetime_property( 'start' ), 'start' ); ?>
    36                 <?php echo Kind_Metabox::kind_the_time( 'mf2_end', __( 'End Time', 'indieweb-post-kinds' ), $kind_post->get_datetime_property( 'end' ), 'end' ); ?>
    37         </p>
     49                                <?php echo Kind_Metabox::kind_the_time( 'mf2_start', __( 'Start Time', 'indieweb-post-kinds' ), $kind_post->get_datetime_property( 'start' ), 'start' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::kind_the_time() escapes its output. ?>
     50                                <?php echo Kind_Metabox::kind_the_time( 'mf2_end', __( 'End Time', 'indieweb-post-kinds' ), $kind_post->get_datetime_property( 'end' ), 'end' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Metabox::kind_the_time() escapes its output. ?>
     51                </p>
    3852</div>
  • indieweb-post-kinds/trunk/views/kind-article.php

    r1828595 r3738687  
    11<?php
    2 /*
    3  * Article Template
     2/**
     3 * Article view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.4.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
  • indieweb-post-kinds/trunk/views/kind-audio.php

    r2406063 r3738687  
    11<?php
    2 /*
    3  * Audio Template
     2/**
     3 * Audio view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
    6 $audios = $kind_post->get_audio();
    7 $duration = null;
     22
     23defined( 'ABSPATH' ) || exit;
     24
     25$audios      = $kind_post->get_audio();
     26$duration    = null;
    827$publication = null;
    928
    … …  
    1130        if ( 1 === count( $audios ) && 0 !== $audios[0] ) {
    1231                $audio_attachment = new Kind_Post( $audios[0] );
    13                 $cite = mf2_to_jf2( $audio_attachment->get_cite() );
     32                $cite             = \ParseThis\mf2_to_jf2( $audio_attachment->get_cite() );
    1433                if ( ! $cite ) {
    1534                        $cite = array();
    … …  
    1837                        $author = Kind_View::get_hcard( $cite['author'] );
    1938                } else {
    20                         $author    = null;
     39                        $author = null;
    2140                }
    2241                if ( array_key_exists( 'duration', $cite ) ) {
    … …  
    3049<section class="response">
    3150<header>
    32 <?php echo Kind_Taxonomy::get_before_kind( 'audio' );
     51<?php
     52echo Kind_Taxonomy::get_before_kind( 'audio' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    3353if ( isset( $cite['name'] ) ) {
    34         echo sprintf( '<span>%1s</a>', $cite['name'] );
     54        printf( '<span class="p-name">%1$s</span>', esc_html( $cite['name'] ) );
    3555}
    3656if ( $duration ) {
    37         printf( '(%1$s)', $duration );
     57        printf( '(%1$s)', $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    3858}
    3959?>
    … …  
    4262<?php
    4363if ( $embed ) {
    44         printf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
     64        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
    4565} elseif ( $audios ) {
    4666        $view = new Kind_Media_View( $audios, 'audio' );
    47         echo $view->get();
     67        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    4868}
  • indieweb-post-kinds/trunk/views/kind-bookmark.php

    r2467905 r3738687  
    11<?php
    2 /*
    3  * Bookmark Template
     2/**
     3 * Bookmark view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1533<header>
    1634<?php
    17 echo Kind_Taxonomy::get_before_kind( 'bookmark' );
     35echo Kind_Taxonomy::get_before_kind( 'bookmark' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1836if ( ! $embed ) {
    19         if ( ! empty( $cite['url'] ) ) {
    20                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $cite['url'], $cite['name'] );
    21         } else {
    22                 echo sprintf( '<span class="p-name">%1s</span>', esc_html( $cite['name'] ) );
     37        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     38        if ( $author ) {
     39                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2340        }
    24         if ( $author ) {
    25                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    26         }
    27         if ( ! empty( $cite['publication'] ) ) {
    28                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', esc_html( $cite['publication'] ) );
     41        $site_name = Kind_View::get_site_name( $cite );
     42        if ( $site_name ) {
     43                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2944        }
    3045}
    … …  
    3348<?php
    3449if ( ! empty( $embed ) ) {
    35         echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    36 } elseif ( ! empty( $cite['summary'] ) ) {
    37         echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     50        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     51} else {
     52        echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3853}
    3954
    40 // Close Response
     55// Close Response.
    4156?>
    4257</section>
  • indieweb-post-kinds/trunk/views/kind-checkin.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Checkin Template
     2/**
     3 * Checkin view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1432<header>
    1533<?php
    16 echo Kind_Taxonomy::get_before_kind( 'checkin' );
     34echo Kind_Taxonomy::get_before_kind( 'checkin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1735if ( ! $embed ) {
    1836        if ( empty( $cite['name'] ) ) {
    … …  
    2038        }
    2139        if ( ! empty( $url ) ) {
    22                 echo sprintf( '<a href="%1s" class="u-checkin h-card">%2s</a>', $url, $cite['name'] );
     40                printf( '<a href="%1$s" class="u-checkin h-card">%2$s</a>', esc_url( $url ), esc_html( $cite['name'] ) );
    2341        } else {
    24                 echo sprintf( '<span class="h-card p-checkin">%1s</span>', $cite['name'] );
     42                printf( '<span class="h-card p-checkin">%1$s</span>', esc_html( $cite['name'] ) );
    2543        }
    2644}
    … …  
    3048if ( $cite ) {
    3149        if ( $embed ) {
    32                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
     50                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
    3351        }
    3452}
    … …  
    3654if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    3755        $view = new Kind_Media_View( $photos, 'photo' );
    38         echo $view->get();
     56        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    3957}
    40 // Close Response
     58// Close Response.
    4159?>
    4260</section>
  • indieweb-post-kinds/trunk/views/kind-drink.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Drink Template
     2/**
     3 * Drink view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1331<header>
    1432<?php
    15 echo Kind_Taxonomy::get_before_kind( 'drink' );
     33echo Kind_Taxonomy::get_before_kind( 'drink' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1634if ( ! $embed ) {
    17         if ( ! empty( $url ) ) {
    18                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    19         } else {
    20                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
    21         }
    22         if ( array_key_exists( 'publication', $cite ) ) {
    23                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     35        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     36        $site_name = Kind_View::get_site_name( $cite );
     37        if ( $site_name ) {
     38                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2439        }
    2540}
    … …  
    2944if ( $cite ) {
    3045        if ( $embed ) {
    31                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    32         } elseif ( array_key_exists( 'summary', $cite ) ) {
    33                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     46                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     47        } else {
     48                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3449        }
    3550}
    … …  
    3752if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    3853        $view = new Kind_Media_View( $photos, 'photo' );
    39         echo $view->get();
     54        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    4055}
    41 // Close Response
     56// Close Response.
    4257?>
    4358</section>
  • indieweb-post-kinds/trunk/views/kind-eat.php

    r3001401 r3738687  
    11<?php
    2 /*
    3  * Eat Template
     2/**
     3 * Eat view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    927}
    1028
    11 $rating  = $kind_post->get( 'rating', true );
     29$rating = $kind_post->get( 'rating', true );
    1230?>
    1331
    … …  
    1634<header>
    1735<?php
    18 echo Kind_Taxonomy::get_before_kind( 'eat' );
     36echo Kind_Taxonomy::get_before_kind( 'eat' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1937if ( ! $embed ) {
    20         if ( ! empty( $url ) ) {
    21                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    22         } else {
    23                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
    24         }
    25         if ( ! empty( $cite['publication'] ) ) {
    26                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     38        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     39        $site_name = Kind_View::get_site_name( $cite );
     40        if ( $site_name ) {
     41                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2742        }
    2843}
    … …  
    3247if ( $cite ) {
    3348        if ( $embed ) {
    34                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    35         } elseif ( array_key_exists( 'summary', $cite ) ) {
    36                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     49                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     50        } else {
     51                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3752        }
    3853}
    3954
    4055if ( $rating ) {
    41         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
    42 } 
     56        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
     57}
    4358
    4459if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    4560        $view = new Kind_Media_View( $photos, 'photo' );
    46         echo $view->get();
     61        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    4762}
    48 // Close Response
     63// Close Response.
    4964?>
    5065</section>
  • indieweb-post-kinds/trunk/views/kind-event.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Event Template
     2/**
     3 * Event view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   3.2.0
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725?>
    … …  
    1028<header>
    1129<?php
    12 echo Kind_Taxonomy::get_before_kind( 'event' );
    13 echo get_the_title();
     30echo Kind_Taxonomy::get_before_kind( 'event' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
     31the_title();
    1432?>
    1533</header>
    1634<?php
    1735$start = $kind_post->get( 'start' );
    18 $end = $kind_post->get( 'end' );
    19 $timestring = '<p>%1$s: <time class="%2$s" datetime="%3$s">%4$s</time></p>';
     36$end   = $kind_post->get( 'end' );
    2037
    2138if ( $start ) {
    22         printf( $timestring, __( 'Start', 'indieweb-post-kinds' ), 'dt-start', $start->format( DATE_W3C ), display_formatted_datetime( $start ) );
    23 } 
     39        printf( '<p>%1$s: <time class="dt-start" datetime="%2$s">%3$s</time></p>', esc_html__( 'Start', 'indieweb-post-kinds' ), esc_attr( $start->format( DATE_W3C ) ), esc_html( Kind_Time::display_formatted_datetime( $start ) ) );
     40}
    2441if ( $end ) {
    25         printf( $timestring, __( 'End', 'indieweb-post-kinds' ), 'dt-end', $end->format( DATE_W3C ), display_formatted_datetime( $end ) );
    26 }       
     42        printf( '<p>%1$s: <time class="dt-end" datetime="%2$s">%3$s</time></p>', esc_html__( 'End', 'indieweb-post-kinds' ), esc_attr( $end->format( DATE_W3C ) ), esc_html( Kind_Time::display_formatted_datetime( $end ) ) );
     43}
    2744if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    2845        $view = new Kind_Media_View( $photos, 'photo' );
    29         echo $view->get();
     46        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    3047}
    31 // Close Response
     48// Close Response.
    3249?>
    3350</section>
  • indieweb-post-kinds/trunk/views/kind-favorite.php

    r2455262 r3738687  
    11<?php
    2 /*
    3  * Favorite Template
     2/**
     3 * Favorite view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1432<header>
    1533<?php
    16 echo Kind_Taxonomy::get_before_kind( 'favorite' );
     34echo Kind_Taxonomy::get_before_kind( 'favorite' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1735if ( ! $embed ) {
    18         if ( ! empty( $url ) ) {
    19                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    20         } else {
    21                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
     36        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     37        if ( $author ) {
     38                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2239        }
    23         if ( $author ) {
    24                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    25         }
    26         if ( ! empty( $cite['publication'] ) ) {
    27                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     40        $site_name = Kind_View::get_site_name( $cite );
     41        if ( $site_name ) {
     42                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2843        }
    2944}
    … …  
    3348if ( $cite ) {
    3449        if ( $embed ) {
    35                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    36         } elseif ( array_key_exists( 'summary', $cite ) ) {
    37                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     50                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     51        } else {
     52                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3853        }
    3954}
    4055
    41 // Close Response
     56// Close Response.
    4257?>
    4358</section>
  • indieweb-post-kinds/trunk/views/kind-issue.php

    r2467905 r3738687  
    11<?php
    2 /*
    3  * Issue Template
     2/**
     3 * Issue view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1432<header>
    1533<?php
    16 echo Kind_Taxonomy::get_before_kind( 'issue' );
     34echo Kind_Taxonomy::get_before_kind( 'issue' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1735if ( ! $embed ) {
    18         if ( ! empty( $url ) ) {
    19                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    20         } else {
    21                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
     36        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     37        if ( $author ) {
     38                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2239        }
    23         if ( $author ) {
    24                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    25         }
    26         if ( ! empty( $cite['publication'] ) ) {
    27                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     40        $site_name = Kind_View::get_site_name( $cite );
     41        if ( $site_name ) {
     42                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2843        }
    2944}
    … …  
    3348if ( $cite ) {
    3449        if ( $embed ) {
    35                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    36         } elseif ( array_key_exists( 'summary', $cite ) ) {
    37                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     50                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     51        } else {
     52                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3853        }
    3954}
    4055
    41 // Close Response
     56// Close Response.
    4257?>
    4358</section>
  • indieweb-post-kinds/trunk/views/kind-itinerary.php

    r2387393 r3738687  
    11<?php
    2 /*
    3  * Event Template
     2/**
     3 * Itinerary view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   3.2.0
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
    622
     23defined( 'ABSPATH' ) || exit;
    724
    825$itineraries = $kind_post->get( 'itinerary', false );
     26$itineraries = is_array( $itineraries ) ? $itineraries : array();
    927
    10 foreach( $itineraries as $key => $value ) {
    11         $itineraries [ $key ] = mf2_to_jf2( $value );
     28foreach ( $itineraries as $key => $value ) {
     29        $leg = \ParseThis\mf2_to_jf2( $value );
     30        // Each field is shown as text; fill in any that are missing.
     31        foreach ( array( 'operator', 'number', 'transit-type', 'origin', 'destination', 'departure', 'arrival' ) as $field ) {
     32                $leg[ $field ] = ( isset( $leg[ $field ] ) && is_scalar( $leg[ $field ] ) ) ? (string) $leg[ $field ] : '';
     33        }
     34        $itineraries[ $key ] = $leg;
    1235}
    1336
    … …  
    1841<header>
    1942<?php
    20 echo Kind_Taxonomy::get_before_kind( 'itinerary' );
    21 echo get_the_title();
     43echo Kind_Taxonomy::get_before_kind( 'itinerary' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
     44the_title();
    2245?>
    2346</header>
    2447<?php
    25 foreach( $itineraries as $itinerary ) {
    26 ?>
    27    <div class="h-leg p-itinerary">
    28                 <h3><span class="p-operator"><?php echo $itinerary['operator']; ?></span>
    29                 <span class="p-number"><?php echo $itinerary['number']; ?></span></h3>
    30                 <data class="p-transit-type" value="<?php echo $itinerary['transit-type']; ?>">
    31    <ul>
     48foreach ( $itineraries as $itinerary ) {
     49        ?>
     50        <div class="h-leg p-itinerary">
     51                <h3><span class="p-operator"><?php echo esc_html( $itinerary['operator'] ); ?></span>
     52                <span class="p-number"><?php echo esc_html( $itinerary['number'] ); ?></span></h3>
     53                <data class="p-transit-type" value="<?php echo esc_attr( $itinerary['transit-type'] ); ?>"></data>
     54        <ul>
    3255        <li>
    33                 <?php _e( 'Departs: ', 'indieweb-post-kinds' ); ?>
    34                 <span class="p-origin"><?php echo $itinerary['origin']; ?></span>
    35                 <time class="dt-departure" datetime="<?php echo $itinerary['departure']; ?>"><?php echo display_formatted_datetime( $itinerary['departure'] ); ?></time>
     56                <?php esc_html_e( 'Departs: ', 'indieweb-post-kinds' ); ?>
     57                <span class="p-origin"><?php echo esc_html( $itinerary['origin'] ); ?></span>
     58                <time class="dt-departure" datetime="<?php echo esc_attr( $itinerary['departure'] ); ?>"><?php echo esc_html( Kind_Time::display_formatted_datetime( $itinerary['departure'] ) ); ?></time>
    3659        </li>
    3760        <li>
    38                 <?php _e( 'Arrives: ', 'indieweb-post-kinds' ); ?>
    39                 <span class="p-destination"><?php echo $itinerary['destination']; ?></span>
    40                 <time class="dt-arrival" datetime="<?php echo $itinerary['arrival']; ?>"><?php echo display_formatted_datetime( $itinerary['arrival'] ); ?></time>
     61                <?php esc_html_e( 'Arrives: ', 'indieweb-post-kinds' ); ?>
     62                <span class="p-destination"><?php echo esc_html( $itinerary['destination'] ); ?></span>
     63                <time class="dt-arrival" datetime="<?php echo esc_attr( $itinerary['arrival'] ); ?>"><?php echo esc_html( Kind_Time::display_formatted_datetime( $itinerary['arrival'] ) ); ?></time>
    4164        </li>
    42    </ul>
    43    </div>
    44 <?php
     65        </ul>
     66        </div>
     67        <?php
    4568}
    4669
    4770
    48 // Close Response
     71// Close Response.
    4972?>
    5073</section>
  • indieweb-post-kinds/trunk/views/kind-jam.php

    r2467905 r3738687  
    11<?php
    2 /*
    3   Jam Template
     2/**
     3 * Jam view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    2038<header>
    2139<?php
    22 echo Kind_Taxonomy::get_before_kind( 'jam' );
     40echo Kind_Taxonomy::get_before_kind( 'jam' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    2341if ( ! $embed ) {
    2442        if ( $title ) {
    25                 echo $title;
     43                echo $title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
    2644        }
    2745        if ( $author ) {
    28                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     46                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2947        }
    3048        if ( $site_name ) {
    31                 echo __( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>';
     49                echo esc_html__( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3250        }
    3351        if ( $duration ) {
    34                 echo Kind_View::display_duration( $duration );
     52                echo Kind_View::display_duration( $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    3553        }
    3654}
    … …  
    4058if ( $cite ) {
    4159        if ( $embed ) {
    42                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    43         } elseif ( array_key_exists( 'summary', $cite ) ) {
    44                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     60                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     61        } else {
     62                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    4563        }
    4664}
    … …  
    4866if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    4967        $view = new Kind_Media_View( $photos, 'photo' );
    50         echo $view->get();
     68        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    5169}
    52 // Close Response
     70// Close Response.
    5371?>
    5472</section>
  • indieweb-post-kinds/trunk/views/kind-like.php

    r2467905 r3738687  
    11<?php
    2 /*
    3  * Like Template
     2/**
     3 * Like view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1533<header>
    1634<?php
    17 echo Kind_Taxonomy::get_before_kind( 'like' );
     35echo Kind_Taxonomy::get_before_kind( 'like' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1836if ( ! $embed ) {
    19         if ( ! empty( $url ) ) {
    20                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    21         } else {
    22                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
     37        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     38        if ( $author ) {
     39                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2340        }
    24         if ( $author ) {
    25                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    26         }
    27         if ( ! empty( $cite['publication'] ) ) {
    28                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     41        $site_name = Kind_View::get_site_name( $cite );
     42        if ( $site_name ) {
     43                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    2944        }
    3045}
    … …  
    3449if ( $cite ) {
    3550        if ( $embed ) {
    36                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    37         } elseif ( array_key_exists( 'summary', $cite ) ) {
    38                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     51                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     52        } else {
     53                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3954        }
    4055}
    4156
    42 // Close Response
     57// Close Response.
    4358?>
    4459</section>
  • indieweb-post-kinds/trunk/views/kind-listen.php

    r3001401 r3738687  
    11<?php
    2 /*
    3   Listen Template
     2/**
     3 * Listen view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1129$title     = Kind_View::get_cite_title( $cite );
    1230$duration  = $kind_post->get( 'duration', true );
    13 $rating  = $kind_post->get( 'rating', true );
     31$rating    = $kind_post->get( 'rating', true );
    1432if ( ! $duration ) {
    1533                $duration = calculate_duration( $kind_post->get( 'start' ), $kind_post->get( 'end' ) );
    … …  
    2240<header>
    2341<?php
    24 echo Kind_Taxonomy::get_before_kind( 'listen' );
     42echo Kind_Taxonomy::get_before_kind( 'listen' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    2543if ( ! $embed ) {
    2644        if ( $title ) {
    27                 echo $title;
     45                echo $title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
    2846        }
    2947        if ( $author ) {
    30                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     48                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    3149        }
    3250        if ( $site_name ) {
    33                 echo __( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>';
     51                echo esc_html__( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3452        }
    3553        if ( $duration ) {
    36                 echo '(' . Kind_View::display_duration( $duration ) . ')';
     54                echo '(' . Kind_View::display_duration( $duration ) . ')'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    3755        }
    3856}
    … …  
    4260if ( $cite ) {
    4361        if ( $embed ) {
    44                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    45         } elseif ( array_key_exists( 'summary', $cite ) ) {
    46                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     62                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     63        } else {
     64                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    4765        }
    4866}
    4967
    50 // Close Response
     68// Close Response.
    5169?>
    5270</section>
    5371
    54 <?php if ( $rating ) {
    55         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
    56 } ?>
     72<?php
     73if ( $rating ) {
     74        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
     75}
     76?>
    5777
    5878<?php
    5979if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    6080        $view = new Kind_Media_View( $photos, 'photo' );
    61         echo $view->get();
     81        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    6282}
    6383?>
  • indieweb-post-kinds/trunk/views/kind-note.php

    r1828595 r3738687  
    11<?php
    2 /*
    3  * Note Template
     2/**
     3 * Note view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.4.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
  • indieweb-post-kinds/trunk/views/kind-photo.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Photo Template
     2/**
     3 * Photo view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
     24
    625if ( $photos ) {
    726        $embed = null;
    … …  
    1130        if ( 1 === count( $photos ) ) {
    1231                $photos_attachment = new Kind_Post( $photos[0] );
    13                 $cite = $photos_attachment->get_cite();
    14                 $cite = $photos_attachment->normalize_cite( $cite );
    15                 $author = Kind_View::get_hcard( $cite['author'] );
     32                $cite              = $photos_attachment->get_cite();
     33                $cite              = $photos_attachment->normalize_cite( $cite );
     34                $author            = Kind_View::get_hcard( $cite['author'] );
    1635        }
    1736}
    … …  
    2140<header>
    2241<?php
    23 echo Kind_Taxonomy::get_before_kind( 'photo' );
     42echo Kind_Taxonomy::get_before_kind( 'photo' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    2443if ( ! empty( $cite['name'] ) ) {
    25         echo sprintf( '<span>%1s</span>', $cite['name'] );
     44        printf( '<span class="p-name">%1$s</span>', esc_html( $cite['name'] ) );
    2645}
    2746?>
    … …  
    2948<?php
    3049if ( $embed ) {
    31         printf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
     50        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
    3251} elseif ( $photos ) {
    3352        $view = new Kind_Media_View( $photos, 'photo' );
    34         echo $view->get();
    35 } ?>
     53        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
     54}
     55?>
    3656</section>
  • indieweb-post-kinds/trunk/views/kind-read.php

    r3001401 r3738687  
    11<?php
    2 /*
    3  * Read Template
     2/**
     3 * Read view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    927}
    1028$read   = $kind_post->get( 'read-status', true );
    11 $rating  = $kind_post->get( 'rating', true );
     29$rating = $kind_post->get( 'rating', true );
    1230
    1331?>
    … …  
    1533<header>
    1634<?php
    17 echo Kind_Taxonomy::get_before_kind( 'read' );
     35echo Kind_Taxonomy::get_before_kind( 'read' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1836if ( ! $embed ) {
    1937        if ( $read ) {
    20                 echo sprintf( ' - <span class="p-read-status">%1s</span>', Kind_View::read_text( $read ) );
     38                printf( ' - <span class="p-read-status">%1$s</span>', esc_html( Kind_View::read_text( $read ) ) );
    2139        }
    22         if ( ! empty( $url ) ) {
    23                 echo sprintf( '<a href="%1s" class="p-name u-url">%2s</a>', $url, $cite['name'] );
    24         } else {
    25                 echo sprintf( '<span class="p-name">%1s</span>', $cite['name'] );
     40        echo Kind_View::get_cite_title( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
     41        if ( $author ) {
     42                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    2643        }
    27         if ( $author ) {
    28                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
    29         }
    30         if ( empty( $cite['publication'] ) ) {
    31                 echo sprintf( ' <em>(<span class="p-publication">%1s</span>)</em>', $cite['publication'] );
     44        $site_name = Kind_View::get_site_name( $cite );
     45        if ( $site_name ) {
     46                echo ' <em>(' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3247        }
    3348}
    … …  
    3752if ( $cite ) {
    3853        if ( $embed ) {
    39                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    40         } elseif ( array_key_exists( 'summary', $cite ) && ! empty( $cite['summary'] ) ) {
    41                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     54                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     55        } else {
     56                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    4257        }
    4358}
    4459
    4560if ( $rating ) {
    46         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
     61        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
    4762}
    4863
    … …  
    5065if ( $photos && ! has_post_thumbnail( get_the_ID() ) ) {
    5166        $view = new Kind_Media_View( $photos, 'photo' );
    52         echo $view->get();
     67        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    5368}
    54 // Close Response
     69// Close Response.
    5570?>
    5671</section>
  • indieweb-post-kinds/trunk/views/kind-rsvp.php

    r2394107 r3738687  
    11<?php
    2 /*
    3  * RSVP Template
     2/**
     3 * RSVP view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    826        return;
    927}
    10 $title  = isset( $cite['name'] ) ? $cite['name'] : $url;
    11 $rsvp   = $kind_post->get( 'rsvp', true );
     28$title = isset( $cite['name'] ) ? $cite['name'] : $url;
     29$rsvp  = $kind_post->get( 'rsvp', true );
    1230
    1331?>
    … …  
    1634<header>
    1735<?php
    18 echo Kind_Taxonomy::get_before_kind( 'rsvp' );
     36echo Kind_Taxonomy::get_before_kind( 'rsvp' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    1937
    2038if ( ! $embed ) {
    2139        if ( $rsvp ) {
    22                 echo '<data class="p-rsvp" value="' . $rsvp . '">' . sprintf( Kind_View::rsvp_text( $rsvp ), $url, $title ) . '</data>';
     40                echo '<data class="p-rsvp" value="' . esc_attr( $rsvp ) . '">' . Kind_View::rsvp_text( $rsvp, $url, $title ) . '</data>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::rsvp_text() escapes the URL and name.
    2341        }
    2442}
    … …  
    2745<?php
    2846if ( $embed ) {
    29         echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    30 } elseif ( array_key_exists( 'summary', $cite ) ) {
    31         echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     47        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     48} else {
     49        echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    3250}
    3351
    34 // Close Response
     52// Close Response.
    3553?>
    3654</section>
  • indieweb-post-kinds/trunk/views/kind-video.php

    r2459401 r3738687  
    11<?php
    2 /*
    3  * Video Template
     2/**
     3 * Video view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
    622
    7 $videos = $kind_post->get_video();
     23defined( 'ABSPATH' ) || exit;
     24
     25$videos   = $kind_post->get_video();
    826$duration = null;
    927if ( is_array( $videos ) ) {
    1028        if ( 1 === count( $videos ) && 0 !== $videos[0] ) {
    1129                $video_attachment = new Kind_Post( $videos[0] );
    12                 $cite = $video_attachment->get_cite();
    13                 $cite = $video_attachment->normalize_cite( $cite );
    14                 $author = Kind_View::get_hcard( $cite['author'] );
    15                
     30                $cite             = $video_attachment->get_cite();
     31                $cite             = $video_attachment->normalize_cite( $cite );
     32                $author           = Kind_View::get_hcard( $cite['author'] );
     33
    1634                if ( array_key_exists( 'duration', $cite ) ) {
    1735                        $duration = Kind_View::display_duration( $cite['duration'] );
    … …  
    2240}
    2341$first_photo = null;
    24 if ( is_countable( $photos ) ) {
     42if ( is_array( $photos ) && ! empty( $photos ) ) {
    2543        $first_photo = $photos[0];
    2644}
    2745if ( is_array( $cite ) && ! $videos ) {
    2846        if ( ! $embed ) {
    29                 $view = new Kind_Media_View( $url, 'video' );
     47                $view  = new Kind_Media_View( $url, 'video' );
    3048                $embed = $view->get();
    3149        }
    … …  
    3755<header>
    3856<?php
    39 echo Kind_Taxonomy::get_before_kind( 'video' );
     57echo Kind_Taxonomy::get_before_kind( 'video' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    4058if ( isset( $cite['name'] ) ) {
    41         echo sprintf( '<span>%1s</a>', $cite['name'] );
     59        printf( '<span class="p-name">%1$s</span>', esc_html( $cite['name'] ) );
    4260}
    4361
    4462if ( $author ) {
    45         echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     63        echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    4664}
    4765if ( $duration ) {
    48         printf( '(%1$s)', $duration );
     66        printf( '(%1$s)', $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    4967}
    5068
    … …  
    5472<?php
    5573if ( $embed ) {
    56         printf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
     74        printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
    5775} elseif ( $videos ) {
    5876
    5977        $poster = wp_get_attachment_image_url( $first_photo, 'full' );
    60         $view = new Kind_Media_View( $videos, 'video' );
    61         echo $view->get();
     78        $view   = new Kind_Media_View( $videos, 'video' );
     79        echo $view->get(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_Media_View::get() returns core gallery, audio or video shortcode markup.
    6280}
    6381?>
  • indieweb-post-kinds/trunk/views/kind-watch.php

    r3001401 r3738687  
    11<?php
    2 /*
    3   Watch Template
     2/**
     3 * Watch view.
    44 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.6.1
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725if ( ! $cite ) {
    … …  
    1129$title     = Kind_View::get_cite_title( $cite, $url );
    1230$duration  = $kind_post->get( 'duration', true );
    13 $rating  = $kind_post->get( 'rating', true );
     31$rating    = $kind_post->get( 'rating', true );
    1432if ( ! $duration ) {
    1533        $duration = calculate_duration( $kind_post->get( 'start' ), $kind_post->get( 'end' ) );
    … …  
    2139<header>
    2240<?php
    23 echo Kind_Taxonomy::get_before_kind( 'watch' );
     41echo Kind_Taxonomy::get_before_kind( 'watch' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    2442if ( ! $embed ) {
    2543        if ( $title ) {
    26                 echo $title;
     44                echo $title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
    2745        }
    2846        if ( $author ) {
    29                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     47                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    3048        }
    3149        if ( $site_name ) {
    32                 echo __( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>';
     50                echo esc_html__( ' from ', 'indieweb-post-kinds' ) . '<em>' . $site_name . '</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3351        }
    3452        if ( $duration ) {
    35                 echo Kind_View::display_duration( $duration );
     53                echo Kind_View::display_duration( $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    3654        }
    3755}
    … …  
    4159if ( $cite ) {
    4260        if ( $embed ) {
    43                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    44         } elseif ( array_key_exists( 'summary', $cite ) ) {
    45                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     61                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     62        } else {
     63                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    4664        }
    4765}
    4866
    49 // Close Response
     67// Close Response.
    5068?>
    5169</section>
    5270
    53 <?php if ( $rating ) {
    54         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
    55 } ?>
     71<?php
     72if ( $rating ) {
     73        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
     74}
     75?>
    5676
    5777
  • indieweb-post-kinds/trunk/views/kind.php

    r3001401 r3738687  
    11<?php
    2 /*
    3   Default Template
    4  *      The Goal of this Template is to be a general all-purpose model that will be replaced by customization in other templates
     2/**
     3 * Default view, used for any kind without its own view.
     4 *
     5 * Rendered by Kind_View::get_view_part(). To change it, copy this file into a
     6 * kind_views folder in your theme; see develop.txt.
     7 *
     8 * @package Post_Kinds
     9 * @since   2.3.0
     10 *
     11 * @var Kind_Post    $kind_post The post being displayed.
     12 * @var string       $kind      The kind's slug, such as 'like'.
     13 * @var string       $type      The kind's microformats property, such as 'like-of'.
     14 * @var array        $cite      The citation, normalized by Kind_Post::normalize_cite(),
     15 *                              so every key is present. 'name' falls back to the URL.
     16 * @var string       $url       The cited URL.
     17 * @var string|false $author    The cited authors as h-card markup, listed as in "Ann, Bob and Cat", or false if there are none.
     18 * @var string       $embed     Embed markup for the cited URL, or an empty string.
     19 * @var int[]|false  $photos    Attachment IDs of the post's photos, from Kind_Post::get_photo().
     20 * @var array        $args      Arguments passed to Kind_View::get_view_part(); 'post_id' is the post's ID.
    521 */
     22
     23defined( 'ABSPATH' ) || exit;
    624
    725$site_name = Kind_View::get_site_name( $cite );
    … …  
    1129                $duration = calculate_duration( $kind_post->get( 'start' ), $kind_post->get( 'end' ) );
    1230}
    13 $rsvp = $kind_post->get( 'rsvp', true );
     31$rsvp   = $kind_post->get( 'rsvp', true );
    1432$rating = $kind_post->get( 'rating', true );
    1533
    … …  
    1836}
    1937
    20 // Add in the appropriate type
     38// Add in the appropriate type.
    2139if ( ! empty( $type ) ) {
    2240        $type = ( empty( $url ) ? 'p-' : 'u-' ) . $type;
    … …  
    2442?>
    2543
    26 <section class="h-cite response <?php echo $type; ?> ">
     44<section class="h-cite response <?php echo esc_attr( $type ); ?>">
    2745<header>
    2846<?php
    29 echo Kind_Taxonomy::get_before_kind( $kind );
     47echo Kind_Taxonomy::get_before_kind( $kind ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_before_kind() escapes the kind name; the icon is the plugin's own SVG.
    3048if ( ! $embed ) {
    3149        if ( $title ) {
    32                 echo $title;
     50                echo $title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_cite_title() escapes its output.
    3351        }
    3452        if ( ! empty( $author ) ) {
    35                 echo ' ' . __( 'by', 'indieweb-post-kinds' ) . ' ' . $author;
     53                echo ' ' . esc_html__( 'by', 'indieweb-post-kinds' ) . ' ' . $author; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- h-cards from Kind_View::get_hcard(), which escapes them.
    3654        }
    3755        if ( $site_name ) {
    38                 echo '<em> (' . $site_name . ')</em>';
     56                echo '<em> (' . $site_name . ')</em>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_site_name() escapes its output.
    3957        }
    4058        if ( in_array( $kind, array( 'jam', 'listen', 'play', 'read', 'watch', 'audio', 'video' ) ) ) {
    4159                if ( $duration ) {
    42                         echo ' ' . Kind_View::display_duration( $duration );
     60                        echo ' ' . Kind_View::display_duration( $duration ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::display_duration() escapes its output.
    4361                }
    4462        }
    … …  
    4967if ( $cite && is_array( $cite ) ) {
    5068        if ( $embed ) {
    51                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $embed );
    52         } elseif ( array_key_exists( 'summary', $cite ) ) {
    53                 echo sprintf( '<blockquote class="e-summary">%1s</blockquote>', $cite['summary'] );
     69                printf( '<blockquote class="e-summary">%1$s</blockquote>', $embed ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- oEmbed or media markup generated by WordPress core.
     70        } else {
     71                echo Kind_View::get_summary( $cite ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Kind_View::get_summary() escapes its output.
    5472        }
    5573}
    5674
    57 // Close Response
     75// Close Response.
    5876?>
    5977</section>
    6078
    61 <?php if ( $rsvp && in_array( $kind, array( 'rsvp' ) ) ) {
    62         echo 'RSVP <span class="p-rsvp">' . $rsvp . '</span>';
     79<?php
     80if ( $rsvp && in_array( $kind, array( 'rsvp' ) ) ) {
     81        echo esc_html__( 'RSVP', 'indieweb-post-kinds' ) . ' <span class="p-rsvp">' . esc_html( $rsvp ) . '</span>';
    6382}
    6483
    6584if ( $rating ) {
    66         echo '<data class="p-rating" value="' . $rating . '">' . sprintf( Kind_View::rating_text( $rating ), $url, $title ) . '</data>';
    67 } ?>
     85        echo '<data class="p-rating" value="' . esc_attr( $rating ) . '">' . esc_html( Kind_View::rating_text( $rating ) ) . '</data>';
     86}
     87?>
    6888
    6989<?php
Note: See TracChangeset for help on using the changeset viewer.