Created
          July 19, 2018 01:01 
        
      - 
      
- 
        Save matjpacker/e73a6c3cf7eb24a3b84649a9f327f064 to your computer and use it in GitHub Desktop. 
    MP-debug
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | [19-Jul-2018 00:59:47 UTC] PHP Notice: Undefined index: content-type in /home/wp_gw6d62/matpacker.com/wp-content/plugins/micropub/micropub.php on line 1236 | |
| [19-Jul-2018 00:59:47 UTC] Micropub Data: {"micropub":"endpoint","q":"config"} {"micropub":"endpoint","q":"config"} | |
| [19-Jul-2018 00:59:47 UTC] PHP Warning: Cannot modify header information - headers already sent by (output started at /home/wp_gw6d62/matpacker.com/wp-content/plugins/micropub/micropub.php:1236) in /home/wp_gw6d62/matpacker.com/wp-content/plugins/micropub/micropub.php on line 1225 | |
| [19-Jul-2018 01:01:10 UTC] Micropub Data: {"micropub":"endpoint"} {"type":["h-entry"],"properties":{"content":["Test post from Quill. "],"category":["test"],"mp-slug":["quill-test"]}} | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | <?php | |
| /** | |
| * Plugin Name: Micropub | |
| * Plugin URI: https://github.com/snarfed/wordpress-micropub | |
| * Description: <a href="https://indiewebcamp.com/micropub">Micropub</a> server. | |
| * Protocol spec: <a href="https://www.w3.org/TR/micropub/">w3.org/TR/micropub</a> | |
| * Author: Ryan Barrett | |
| * Author URI: https://snarfed.org/ | |
| * Text Domain: micropub | |
| * Version: 1.4.3 | |
| */ | |
| /* See README for supported filters and actions. | |
| * Example command lines for testing: | |
| * Form-encoded: | |
| * curl -i -H 'Authorization: Bearer ...' -F h=entry -F name=foo -F content=bar \ | |
| * -F photo=@gallery/snarfed.gif 'http://localhost/w/?micropub=endpoint' | |
| * JSON: | |
| * curl -v -d @body.json -H 'Content-Type: application/json' 'http://localhost/w/?micropub=endpoint' | |
| * | |
| * To generate an access token for testing: | |
| * 1. Open this in a browser, filling in SITE: | |
| * https://indieauth.com/auth?me=SITE&scope=post&client_id=https://wordpress.org/plugins/micropub/&redirect_uri=https%3A%2F%2Findieauth.com%2Fsuccess | |
| * 2. Log in. | |
| * 3. Extract the code param from the URL. | |
| * 4. Run this command line, filling in CODE and SITE (which logged into IndieAuth): | |
| * curl -i -d 'code=CODE&me=SITE&client_id=indieauth&redirect_uri=https://indieauth.com/success' 'https://tokens.indieauth.com/token' | |
| * 5. Extract the access_token parameter from the response body. | |
| */ | |
| /* For debugging purposes this will bypass Micropub authentication | |
| * in favor of WordPress authentication | |
| * Using this to test querying(q=) parameters quickly | |
| */ | |
| if ( ! defined( 'MICROPUB_LOCAL_AUTH' ) ) { | |
| define( 'MICROPUB_LOCAL_AUTH', '0' ); | |
| } | |
| // Allows for a custom Authentication and Token Endpoint | |
| if ( ! defined( 'MICROPUB_AUTHENTICATION_ENDPOINT' ) ) { | |
| define( 'MICROPUB_AUTHENTICATION_ENDPOINT', 'https://indieauth.com/auth' ); | |
| } | |
| if ( ! defined( 'MICROPUB_TOKEN_ENDPOINT' ) ) { | |
| define( 'MICROPUB_TOKEN_ENDPOINT', 'https://tokens.indieauth.com/token' ); | |
| } | |
| // For debugging purposes this will set all Micropub posts to Draft | |
| if ( ! defined( 'MICROPUB_DRAFT_MODE' ) ) { | |
| define( 'MICROPUB_DRAFT_MODE', '0' ); | |
| } | |
| add_action( 'plugins_loaded', array( 'Micropub_Plugin', 'init' ) ); | |
| /** | |
| * Micropub Plugin Class | |
| */ | |
| class Micropub_Plugin { | |
| // associative array | |
| public static $request_headers; | |
| // associative array, read from JSON or form-encoded input. populated by load_input(). | |
| protected static $input; | |
| // associative array, populated by authorize(). | |
| protected static $micropub_auth_response; | |
| // Array of Scopes | |
| protected static $scopes; | |
| /** | |
| * Initialize the plugin. | |
| */ | |
| public static function init() { | |
| $cls = get_called_class(); | |
| add_action( 'admin_init', array( $cls, 'admin_init' ) ); | |
| // register endpoint | |
| // (I originally used add_rewrite_endpoint() to serve on /micropub instead | |
| // of ?micropub=endpoint, but that had problems. details in | |
| // https://github.com/snarfed/wordpress-micropub/commit/d3bdc433ee019d3968be6c195b0384cba5ffe36b#commitcomment-9690066 ) | |
| add_filter( 'query_vars', array( $cls, 'query_var' ) ); | |
| add_action( 'parse_query', array( $cls, 'parse_query' ) ); | |
| // endpoint discovery | |
| add_action( 'wp_head', array( $cls, 'micropub_html_header' ), 99 ); | |
| add_action( 'send_headers', array( $cls, 'micropub_http_header' ) ); | |
| add_filter( 'host_meta', array( $cls, 'micropub_jrd_links' ) ); | |
| add_filter( 'webfinger_user_data', array( $cls, 'micropub_jrd_links' ) ); | |
| // Disable adding headers if local auth is set | |
| if ( MICROPUB_LOCAL_AUTH || ! class_exists( 'IndieAuth_Plugin' ) ) { | |
| add_action( 'wp_head', array( $cls, 'indieauth_html_header' ), 99 ); | |
| add_action( 'send_headers', array( $cls, 'indieauth_http_header' ) ); | |
| add_filter( 'host_meta', array( $cls, 'indieauth_jrd_links' ) ); | |
| add_filter( 'webfinger_user_data', array( $cls, 'indieauth_jrd_links' ) ); | |
| } | |
| // Post Content Filter | |
| add_filter( 'micropub_post_content', array( $cls, 'generate_post_content' ), 1, 2 ); | |
| // Register Setting | |
| register_setting( | |
| 'writing', // Option Group Writing | |
| 'micropub_default_post_status', // Setting Name | |
| array( | |
| 'type' => 'string', | |
| 'description' => 'Default Post Status for Micropub Server', | |
| 'sanitize_callback' => 'sanitize_text_field', | |
| 'show_in_rest' => false, | |
| 'default' => static::default_post_status(), | |
| ) | |
| ); | |
| } | |
| public static function admin_init() { | |
| $cls = get_called_class(); | |
| add_settings_field( | |
| 'micropub_writing_settings', | |
| __( 'Default Status for Micropub Posts', 'micropub' ), | |
| array( $cls, 'writing_settings' ), | |
| 'writing', | |
| 'default' | |
| ); | |
| } | |
| public static function writing_settings() { | |
| load_template( plugin_dir_path( __FILE__ ) . 'templates/micropub-writing-settings.php' ); | |
| } | |
| public static function get( $array, $key, $default = array() ) { | |
| if ( is_array( $array ) ) { | |
| return isset( $array[ $key ] ) ? $array[ $key ] : $default; | |
| } | |
| return $default; | |
| } | |
| /** | |
| * Adds some query vars | |
| * | |
| * @param array $vars | |
| * @return array | |
| */ | |
| public static function query_var( $vars ) { | |
| $vars[] = 'micropub'; | |
| return $vars; | |
| } | |
| /** | |
| * Parse the micropub request and render the document | |
| * | |
| * @param WP $wp WordPress request context | |
| * | |
| * @uses apply_filter() Calls 'before_micropub' on the default request | |
| */ | |
| public static function parse_query( $wp ) { | |
| if ( ! array_key_exists( 'micropub', $wp->query_vars ) ) { | |
| return; | |
| } | |
| static::load_input(); | |
| if ( WP_DEBUG ) { | |
| error_log( | |
| 'Micropub Data: ' . wp_json_encode( $_GET ) . ' ' . | |
| wp_json_encode( static::$input ) | |
| ); | |
| } | |
| static::$input = apply_filters( 'before_micropub', static::$input ); | |
| // Be able to bypass Micropub auth with other auth | |
| if ( MICROPUB_LOCAL_AUTH || class_exists( 'IndieAuth_Plugin' ) ) { | |
| $user_id = get_current_user_id(); | |
| // The WordPress IndieAuth plugin uses filters for this | |
| static::$scopes = apply_filters( 'indieauth_scopes', static::$scopes ); | |
| static::$micropub_auth_response = apply_filters( 'indieauth_response', static::$micropub_auth_response ); | |
| if ( ! $user_id ) { | |
| static::handle_authorize_error( 401, 'Unauthorized' ); | |
| } | |
| } else { | |
| $user_id = static::authorize(); | |
| } | |
| if ( 'GET' === $_SERVER['REQUEST_METHOD'] && isset( static::$input['q'] ) ) { | |
| static::query_handler( $user_id ); | |
| } elseif ( 'POST' === $_SERVER['REQUEST_METHOD'] ) { | |
| self::post_handler( $user_id ); | |
| } else { | |
| static::error( 400, 'Unknown Micropub request' ); | |
| } | |
| } | |
| /** | |
| * Validate the access token at the token endpoint. | |
| * | |
| * https://indieauth.spec.indieweb.org/#access-token-verification | |
| * If the token is valid, returns the user id to use as the post's author, or | |
| * NULL if the token only matched the site URL and no specific user. | |
| */ | |
| private static function authorize() { | |
| // find the access token | |
| $auth = static::get_header( 'authorization' ); | |
| $token = self::get( $_POST, 'access_token' ); | |
| if ( ! $auth && ! $token ) { | |
| static::handle_authorize_error( 401, 'missing access token' ); | |
| } | |
| $resp = wp_remote_get( | |
| get_option( 'indieauth_token_endpoint', MICROPUB_TOKEN_ENDPOINT ), array( | |
| 'headers' => array( | |
| 'Accept' => 'application/json', | |
| 'Authorization' => $auth ?: 'Bearer ' . $token, | |
| ), | |
| ) | |
| ); | |
| if ( is_wp_error( $resp ) ) { | |
| static::handle_authorize_error( 502, "couldn't validate token: " . implode( ' , ', $resp->get_error_messages() ) ); | |
| } | |
| $code = wp_remote_retrieve_response_code( $resp ); | |
| $body = wp_remote_retrieve_body( $resp ); | |
| $params = json_decode( $body, true ); | |
| static::$scopes = explode( ' ', $params['scope'] ); | |
| if ( (int) ( $code / 100 ) !== 2 ) { | |
| static::handle_authorize_error( | |
| $code, 'invalid access token: ' . $body | |
| ); | |
| } elseif ( empty( static::$scopes ) ) { | |
| static::handle_authorize_error( | |
| 401, 'access token is missing scope' | |
| ); | |
| } | |
| $me = untrailingslashit( $params['me'] ); | |
| static::$micropub_auth_response = $params; | |
| // look for a user with the same url as the token's `me` value. | |
| $user = static::user_url( $me ); | |
| if ( $user ) { | |
| return $user; | |
| } | |
| // no user with that url. if the token is for this site itself, allow it and | |
| // post as the default user | |
| $home = untrailingslashit( home_url() ); | |
| if ( $home !== $me ) { | |
| static::handle_authorize_error( | |
| 401, 'access token URL ' . $me . " doesn't match site " . $home . ' or any user' | |
| ); | |
| } | |
| return null; | |
| } | |
| /** | |
| * Check scope | |
| * | |
| * @param string $scope | |
| * | |
| * @return boolean | |
| **/ | |
| protected static function check_scope( $scope ) { | |
| if ( in_array( 'post', static::$scopes, true ) ) { | |
| return true; | |
| } | |
| return in_array( $scope, static::$scopes, true ); | |
| } | |
| /** | |
| * Try to match a user with a URL with or without trailing slash. | |
| * | |
| * @param string $me URL to match | |
| * | |
| * @return null|int Return user ID if matched or null | |
| **/ | |
| public static function user_url( $me ) { | |
| if ( ! isset( $me ) ) { | |
| return null; | |
| } | |
| $search = array( | |
| 'search' => $me, | |
| 'search_columns' => array( 'url' ), | |
| ); | |
| $users = get_users( $search ); | |
| $search['search'] = $me . '/'; | |
| $users = array_merge( $users, get_users( $search ) ); | |
| foreach ( $users as $user ) { | |
| if ( untrailingslashit( $user->user_url ) === $me ) { | |
| return $user->ID; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Parse the micropub request and render the document | |
| * | |
| * @param int $user_id User ID for Authorized User. | |
| */ | |
| public static function post_handler( $user_id ) { | |
| $status = 200; | |
| $action = self::get( static::$input, 'action', 'create' ); | |
| if ( ! self::check_scope( $action ) ) { | |
| static::error( 403, sprintf( 'scope insufficient to %1$s posts', $action ) ); | |
| } | |
| $url = self::get( static::$input, 'url' ); | |
| // check that we support all requested syndication targets | |
| $synd_supported = apply_filters( 'micropub_syndicate-to', array(), $user_id ); | |
| $uids = array(); | |
| foreach ( $synd_supported as $syn ) { | |
| $uids[] = self::get( $syn, 'uid' ); | |
| } | |
| $properties = self::get( static::$input, 'properties' ); | |
| $synd_requested = self::get( $properties, 'mp-syndicate-to' ); | |
| $unknown = array_diff( $synd_requested, $uids ); | |
| if ( $unknown ) { | |
| static::error( 400, 'Unknown mp-syndicate-to targets: ' . implode( ', ', $unknown ) ); | |
| } elseif ( ! $url || 'create' === $action ) { // create | |
| if ( $user_id && ! user_can( $user_id, 'publish_posts' ) ) { | |
| static::error( 403, 'user id ' . $user_id . ' cannot publish posts' ); | |
| } | |
| $args = static::create( $user_id ); | |
| $status = 201; | |
| static::header( 'Location', get_permalink( $args['ID'] ) ); | |
| } elseif ( 'update' === $action || ! $action ) { // update | |
| if ( $user_id && ! user_can( $user_id, 'edit_posts' ) ) { | |
| static::error( 403, 'user id ' . $user_id . ' cannot edit posts' ); | |
| } | |
| $args = static::update(); | |
| } elseif ( 'delete' === $action ) { // delete | |
| if ( $user_id && ! user_can( $user_id, 'delete_posts' ) ) { | |
| static::error( 403, 'user id ' . $user_id . ' cannot delete posts' ); | |
| } | |
| $args = get_post( url_to_postid( $url ), ARRAY_A ); | |
| if ( ! $args ) { | |
| static::error( 400, $url . ' not found' ); | |
| } | |
| static::check_error( wp_trash_post( $args['ID'] ) ); | |
| } elseif ( 'undelete' === $action ) { // undelete | |
| if ( $user_id && ! user_can( $user_id, 'publish_posts' ) ) { | |
| static::error( 403, 'user id ' . $user_id . ' cannot undelete posts' ); | |
| } | |
| $found = false; | |
| // url_to_postid() doesn't support posts in trash, so look for | |
| // it ourselves, manually. | |
| // here's another, more complicated way that customizes WP_Query: | |
| // https://gist.github.com/peterwilsoncc/bb40e52cae7faa0e6efc | |
| foreach ( get_posts( array( 'post_status' => 'trash' ) ) as $post ) { | |
| if ( get_the_guid( $post ) === $url ) { | |
| wp_untrash_post( $post->ID ); | |
| wp_publish_post( $post->ID ); | |
| $found = true; | |
| $args = array( 'ID' => $post->ID ); | |
| } | |
| } | |
| if ( ! $found ) { | |
| static::error( 400, 'deleted post ' . $url . ' not found' ); | |
| } | |
| // unknown action | |
| } else { | |
| static::error( 400, 'unknown action ' . $action ); | |
| } | |
| if ( ! empty( $synd_requested ) ) { | |
| do_action( 'micropub_syndication', $args['ID'], $synd_requested ); | |
| } | |
| do_action( 'after_micropub', static::$input, $args ); | |
| static::respond( $status, null, $args ); | |
| } | |
| /** | |
| * Handle queries to the micropub endpoint | |
| * | |
| * @param int $user_id Authenticated User | |
| */ | |
| private static function query_handler( $user_id ) { | |
| $resp = apply_filters( 'micropub_query', null, static::$input ); | |
| if ( ! $resp ) { | |
| switch ( static::$input['q'] ) { | |
| case 'config': | |
| case 'syndicate-to': | |
| // return empty syndication target with filter | |
| $syndicate_tos = apply_filters( 'micropub_syndicate-to', array(), $user_id ); | |
| $resp = array( 'syndicate-to' => $syndicate_tos ); | |
| break; | |
| case 'category': | |
| $resp = array_merge( | |
| get_tags( array( 'fields' => 'names' ) ), | |
| get_terms( | |
| array( | |
| 'taxonomy' => 'category', | |
| 'fields' => 'names', | |
| ) | |
| ) | |
| ); | |
| break; | |
| case 'source': | |
| $post_id = url_to_postid( static::$input['url'] ); | |
| if ( ! $post_id ) { | |
| static::error( 400, 'not found: ' . static::$input['url'] ); | |
| } | |
| $resp = static::get_mf2( $post_id ); | |
| $props = static::$input['properties']; | |
| if ( $props ) { | |
| if ( ! is_array( $props ) ) { | |
| $props = array( $props ); | |
| } | |
| $resp = array( | |
| 'properties' => array_intersect_key( | |
| $resp['properties'], array_flip( $props ) | |
| ), | |
| ); | |
| } | |
| break; | |
| default: | |
| static::error( 400, 'unknown query ' . static::$input['q'] ); | |
| } | |
| } | |
| do_action( 'after_micropub', static::$input, null ); | |
| static::respond( 200, $resp ); | |
| } | |
| /* | |
| * Handle a create request. | |
| */ | |
| private static function create( $user_id ) { | |
| $args = static::mp_to_wp( static::$input ); | |
| $args = static::store_micropub_auth_response( $args ); | |
| $post_content = self::get( $args, 'post_content', '' ); | |
| $post_content = apply_filters( 'micropub_post_content', $post_content, static::$input ); | |
| if ( $post_content ) { | |
| $args['post_content'] = $post_content; | |
| } | |
| $args = static::store_mf2( $args ); | |
| $args = static::store_geodata( $args ); | |
| if ( $user_id ) { | |
| $args['post_author'] = $user_id; | |
| } | |
| $args['post_status'] = static::post_status( static::$input ); | |
| if ( ! $args['post_status'] ) { | |
| static::error( 400, 'Invalid Post Status' ); | |
| } | |
| if ( WP_DEBUG ) { | |
| error_log( 'wp_insert_post with args: ' . wp_json_encode( $args ) ); | |
| } | |
| kses_remove_filters(); // prevent sanitizing HTML tags in post_content | |
| $args['ID'] = static::check_error( wp_insert_post( $args, true ) ); | |
| kses_init_filters(); | |
| static::default_file_handler( $args['ID'] ); | |
| return $args; | |
| } | |
| /* | |
| * Handle an update request. | |
| * | |
| * This really needs a db transaction! But we can't assume the underlying | |
| * MySQL db is InnoDB and supports transactions. :( | |
| */ | |
| private static function update() { | |
| $post_id = url_to_postid( static::$input['url'] ); | |
| $args = get_post( $post_id, ARRAY_A ); | |
| if ( ! $args ) { | |
| static::error( 400, static::$input['url'] . ' not found' ); | |
| } | |
| // add | |
| $add = static::$input['add']; | |
| if ( $add ) { | |
| if ( ! is_array( $add ) ) { | |
| static::error( 400, 'add must be an object' ); | |
| } | |
| if ( array_diff( array_keys( $add ), array( 'category', 'syndication' ) ) ) { | |
| static::error( 400, 'can only add to category and syndication; other properties not supported' ); | |
| } | |
| $add_args = static::mp_to_wp( array( 'properties' => $add ) ); | |
| if ( $add_args['tags_input'] ) { | |
| // i tried wp_add_post_tags here, but it didn't work | |
| $args['tags_input'] = array_merge( | |
| $args['tags_input'] ?: array(), | |
| $add_args['tags_input'] | |
| ); | |
| } | |
| if ( $add_args['post_category'] ) { | |
| // i tried wp_set_post_categories here, but it didn't work | |
| $args['post_category'] = array_merge( | |
| $args['post_category'] ?: array(), | |
| $add_args['post_category'] | |
| ); | |
| } | |
| } | |
| // replace | |
| $replace = static::$input['replace']; | |
| if ( $replace ) { | |
| if ( ! is_array( $replace ) ) { | |
| static::error( 400, 'replace must be an object' ); | |
| } | |
| foreach ( static::mp_to_wp( array( 'properties' => $replace ) ) | |
| as $name => $val ) { | |
| $args[ $name ] = $val; | |
| } | |
| } | |
| // delete | |
| $delete = static::$input['delete']; | |
| if ( $delete ) { | |
| if ( is_assoc_array( $delete ) ) { | |
| if ( array_diff( array_keys( $delete ), array( 'category', 'syndication' ) ) ) { | |
| static::error( 400, 'can only delete individual values from category and syndication; other properties not supported' ); | |
| } | |
| $delete_args = static::mp_to_wp( array( 'properties' => $delete ) ); | |
| if ( $delete_args['tags_input'] ) { | |
| $args['tags_input'] = array_diff( | |
| $args['tags_input'] ?: array(), | |
| $delete_args['tags_input'] | |
| ); | |
| } | |
| if ( $delete_args['post_category'] ) { | |
| $args['post_category'] = array_diff( | |
| $args['post_category'] ?: array(), | |
| $delete_args['post_category'] | |
| ); | |
| } | |
| } elseif ( is_array( $delete ) ) { | |
| foreach ( static::mp_to_wp( array( 'properties' => array_flip( $delete ) ) ) | |
| as $name => $_ ) { | |
| $args[ $name ] = null; | |
| } | |
| if ( in_array( 'category', $delete, true ) ) { | |
| wp_set_post_tags( $post_id, '', false ); | |
| wp_set_post_categories( $post_id, '' ); | |
| } | |
| } else { | |
| static::error( 400, 'delete must be an array or object' ); | |
| } | |
| } | |
| // tell WordPress to preserve published date explicitly, otherwise | |
| // wp_update_post sets it to the current time | |
| $args['edit_date'] = true; | |
| // Generate Post Content | |
| $post_content = self::get( $args, 'post_content', '' ); | |
| $post_content = apply_filters( 'micropub_post_content', $post_content, static::$input ); | |
| if ( $post_content ) { | |
| $args['post_content'] = $post_content; | |
| } | |
| // Store metadata from Microformats Properties | |
| $args = static::store_mf2( $args ); | |
| $args = static::store_geodata( $args ); | |
| if ( WP_DEBUG ) { | |
| error_log( 'wp_update_post with args: ' . wp_json_encode( $args ) ); | |
| } | |
| kses_remove_filters(); | |
| static::check_error( wp_update_post( $args, true ) ); | |
| kses_init_filters(); | |
| static::default_file_handler( $post_id ); | |
| return $args; | |
| } | |
| private static function handle_authorize_error( $code, $msg ) { | |
| $home = untrailingslashit( home_url() ); | |
| if ( 'http://localhost' === $home ) { | |
| error_log( | |
| 'WARNING: ' . $code . ' ' . $msg . | |
| ". Allowing only because this is localhost.\n" | |
| ); | |
| return; | |
| } | |
| static::respond( $code, $msg ); | |
| } | |
| private static function default_post_status() { | |
| return MICROPUB_DRAFT_MODE ? 'draft' : 'publish'; | |
| } | |
| private static function post_status( $mf2 ) { | |
| $props = $mf2['properties']; | |
| // If both are not set immediately return | |
| if ( ! isset( $props['post-status'] ) && ! isset( $props['visibility'] ) ) { | |
| $status = get_option( 'micropub_default_post_status', static::default_post_status() ); | |
| if ( ! in_array( $status, array( 'publish', 'draft', 'private' ), true ) ) { | |
| return static::default_post_status(); | |
| } | |
| return $status; | |
| } | |
| if ( isset( $props['visibility'] ) ) { | |
| $visibilitylist = array( array( 'private' ), array( 'public' ) ); | |
| if ( ! in_array( $props['visibility'], $visibilitylist, true ) ) { | |
| // Returning null will cause the server to return a 400 error | |
| return null; | |
| } | |
| if ( array( 'private' ) === $props['visibility'] ) { | |
| return 'private'; | |
| } | |
| } | |
| if ( isset( $props['post-status'] ) ) { | |
| // According to the proposed specification these are the only two properties supported. | |
| // https://indieweb.org/Micropub-extensions#Post_Status | |
| // For now these are the only two we will support even though WordPress defaults to 8 and allows custom | |
| // But makes it easy to change | |
| $statuslist = array( array( 'published' ), array( 'draft' ) ); | |
| if ( ! in_array( $props['post-status'], $statuslist, true ) ) { | |
| // Returning null will cause the server to return a 400 error | |
| return null; | |
| } | |
| // Map published to the WordPress property publish. | |
| if ( array( 'published' ) === $props['post-status'] ) { | |
| return 'publish'; | |
| } | |
| return 'draft'; | |
| } | |
| // Execution will never reach here | |
| } | |
| /** | |
| * Converts Micropub create, update, or delete request to args for WordPress | |
| * wp_insert_post() or wp_update_post(). | |
| * | |
| * For updates, reads the existing post and starts with its data: | |
| * 'replace' properties are replaced | |
| * 'add' properties are added. the new value in $args will contain both the | |
| * existing and new values. | |
| * 'delete' properties are set to NULL | |
| * | |
| * Uses $input, so load_input() must be called before this. | |
| */ | |
| private static function mp_to_wp( $mf2 ) { | |
| $props = $mf2['properties']; | |
| $args = array(); | |
| foreach ( array( | |
| 'mp-slug' => 'post_name', | |
| 'name' => 'post_title', | |
| 'summary' => 'post_excerpt', | |
| ) as $mf => $wp ) { | |
| if ( isset( $props[ $mf ] ) ) { | |
| $args[ $wp ] = static::get( $props[ $mf ], 0 ); | |
| } | |
| } | |
| // perform these functions only for creates | |
| if ( ! isset( $args['ID'] ) ) { | |
| if ( isset( $args['post_title'] ) && ! isset( $args['post_name'] ) ) { | |
| $args['post_name'] = $args['post_title']; | |
| } | |
| } | |
| if ( isset( $args['post_name'] ) ) { | |
| $args['post_name'] = sanitize_title( $args['post_name'] ); | |
| } | |
| if ( isset( $props['published'] ) ) { | |
| $date = new DateTime( $props['published'][0] ); | |
| // If for whatever reason the date cannot be parsed do not include one which defaults to now | |
| if ( $date ) { | |
| $tz_string = get_option( 'timezone_string' ); | |
| if ( empty( $tz_string ) ) { | |
| $tz_string = 'UTC'; | |
| } | |
| $date->setTimeZone( new DateTimeZone( $tz_string ) ); | |
| $tz = $date->getTimezone(); | |
| // Pass this argument to the filter for use | |
| $args['timezone'] = $tz->getName(); | |
| $args['post_date'] = $date->format( 'Y-m-d H:i:s' ); | |
| $date->setTimeZone( new DateTimeZone( 'GMT' ) ); | |
| $args['post_date_gmt'] = $date->format( 'Y-m-d H:i:s' ); | |
| } | |
| } | |
| // Map micropub categories to WordPress categories if they exist, otherwise | |
| // to WordPress tags. | |
| if ( isset( $props['category'] ) ) { | |
| $args['post_category'] = array(); | |
| $args['tags_input'] = array(); | |
| foreach ( $props['category'] as $mp_cat ) { | |
| $wp_cat = get_category_by_slug( $mp_cat ); | |
| if ( $wp_cat ) { | |
| $args['post_category'][] = $wp_cat->term_id; | |
| } else { | |
| $args['tags_input'][] = $mp_cat; | |
| } | |
| } | |
| } | |
| $content = $props['content'][0]; | |
| if ( is_array( $content ) ) { | |
| $args['post_content'] = $content['html'] ?: | |
| htmlspecialchars( $content['value'] ); | |
| } elseif ( $content ) { | |
| $args['post_content'] = htmlspecialchars( $content ); | |
| } elseif ( $props['summary'] ) { | |
| $args['post_content'] = $props['summary'][0]; | |
| } | |
| return $args; | |
| } | |
| /** | |
| * Generates and returns a post_content string suitable for wp_insert_post() | |
| * and friends. | |
| */ | |
| public static function generate_post_content( $post_content, $input ) { | |
| $props = $input['replace'] ?: $input['properties']; | |
| $lines = array(); | |
| $verbs = array( | |
| 'like-of' => 'Likes', | |
| 'repost-of' => 'Reposted', | |
| 'in-reply-to' => 'In reply to', | |
| 'bookmark-of' => 'Bookmarked', | |
| ); | |
| // interactions | |
| foreach ( array_keys( $verbs ) as $prop ) { | |
| if ( wp_is_numeric_array( $props[ $prop ] ) ) { | |
| $val = $props[ $prop ][0]; | |
| } else { | |
| $val = $props[ $prop ]; | |
| } | |
| if ( $val ) { | |
| // Supports nested properties by turning single value properties into nested | |
| // https://micropub.net/draft/#nested-microformats-objects | |
| if ( is_string( $val ) ) { | |
| $val = array( | |
| 'url' => $val, | |
| ); | |
| } | |
| if ( ! isset( $val['name'] ) && isset( $val['url'] ) ) { | |
| $val['name'] = $val['url']; | |
| } | |
| if ( isset( $val['url'] ) ) { | |
| $lines[] = sprintf( | |
| '<p>%1s <a class="u-%2s" href="%3s">%4s</a>.</p>', | |
| $verbs[ $prop ], | |
| $prop, $val['url'], | |
| $val['name'] | |
| ); | |
| } | |
| } | |
| } | |
| $checkin = isset( $props['checkin'] ) ? $props['checkin'][0] : null; | |
| if ( $checkin ) { | |
| $name = $checkin['properties']['name'][0]; | |
| $urls = $checkin['properties']['url']; | |
| $lines[] = '<p>Checked into <a class="h-card p-location" href="' . | |
| ( $urls[1] ?: $urls[0] ) . '">' . $name . '</a>.</p>'; | |
| } | |
| if ( isset( $props['rsvp'] ) ) { | |
| $lines[] = '<p>RSVPs <data class="p-rsvp" value="' . $props['rsvp'][0] . | |
| '">' . $props['rsvp'][0] . '</data>.</p>'; | |
| } | |
| // event | |
| if ( array( 'h-event' ) === static::$input['type'] ) { | |
| $lines[] = static::generate_event( static::$input ); | |
| } | |
| // content | |
| $content = $props['content'][0]; | |
| if ( $content ) { | |
| $lines[] = '<div class="e-content">'; | |
| if ( is_array( $content ) ) { | |
| $lines[] = $content['html'] ?: htmlspecialchars( $content['value'] ); | |
| } else { | |
| $lines[] = htmlspecialchars( $content ); | |
| } | |
| $lines[] = '</div>'; | |
| } | |
| // TODO: generate my own markup so i can include u-photo | |
| foreach ( array( 'photo', 'video', 'audio' ) as $field ) { | |
| if ( isset( $_FILES[ $field ] ) || isset( $props[ $field ] ) ) { | |
| $lines[] = '[gallery size=full columns=1]'; | |
| break; | |
| } | |
| } | |
| return implode( "\n", $lines ); | |
| } | |
| /** | |
| * Generates and returns a string h-event. | |
| */ | |
| private static function generate_event( $input ) { | |
| $props = $input['replace'] ?: $input['properties']; | |
| $lines[] = '<div class="h-event">'; | |
| if ( isset( $props['name'] ) ) { | |
| $lines[] = '<h1 class="p-name">' . $props['name'][0] . '</h1>'; | |
| } | |
| $lines[] = '<p>'; | |
| $times = array(); | |
| foreach ( array( 'start', 'end' ) as $cls ) { | |
| if ( isset( $props[ $cls ][0] ) ) { | |
| $datetime = iso8601_to_datetime( $props[ $cls ][0] ); | |
| $times[] = '<time class="dt-' . $cls . '" datetime="' . | |
| $props[ $cls ][0] . '">' . $datetime . '</time>'; | |
| } | |
| } | |
| $lines[] = implode( "\nto\n", $times ); | |
| if ( isset( $props['location'] ) && 'geo:' !== substr( $props['location'][0], 0, 4 ) ) { | |
| $lines[] = 'at <a class="p-location" href="' . $props['location'][0] . '">' . | |
| $props['location'][0] . '</a>'; | |
| } | |
| end( $lines ); | |
| $lines[ key( $lines ) ] .= '.'; | |
| $lines[] = '</p>'; | |
| if ( isset( $props['summary'] ) ) { | |
| $lines[] = '<p class="p-summary">' . urldecode( $props['summary'][0] ) . '</p>'; | |
| } | |
| if ( isset( $props['description'] ) ) { | |
| $lines[] = '<p class="p-description">' . urldecode( $props['description'][0] ) . '</p>'; | |
| } | |
| $lines[] = '</div>'; | |
| return implode( "\n", $lines ); | |
| } | |
| /** | |
| * Handles Photo Upload. | |
| * | |
| */ | |
| public static function default_file_handler( $post_id ) { | |
| foreach ( array( 'photo', 'video', 'audio' ) as $field ) { | |
| $props = static::$input['properties']; | |
| $att_ids = array(); | |
| if ( isset( $_FILES[ $field ] ) || isset( $props[ $field ] ) ) { | |
| require_once ABSPATH . 'wp-admin/includes/image.php'; | |
| require_once ABSPATH . 'wp-admin/includes/file.php'; | |
| require_once ABSPATH . 'wp-admin/includes/media.php'; | |
| if ( isset( $_FILES[ $field ] ) ) { | |
| $overrides = array( | |
| 'action' => 'allow_file_outside_uploads_dir', | |
| 'test_form' => false, | |
| ); | |
| $files = $_FILES[ $field ]; | |
| if ( is_array( $files['name'] ) ) { | |
| $count = count( $files['name'] ); | |
| for ( $i = 0; $i < $count; ++$i ) { | |
| $_FILES = array( | |
| $field => array( | |
| 'name' => $files['name'][ $i ], | |
| 'tmp_name' => $files['tmp_name'][ $i ], | |
| // 'type' => $files['type'][ $i ], | |
| // 'error' => $files['error'][ $i ], | |
| 'size' => $files['size'][ $i ], | |
| ), | |
| ); | |
| $att_ids[] = static::check_error( | |
| media_handle_upload( | |
| $field, $post_id, array(), $overrides | |
| ) | |
| ); | |
| } | |
| } else { | |
| $att_ids[] = static::check_error( | |
| media_handle_upload( | |
| $field, $post_id, array(), $overrides | |
| ) | |
| ); | |
| } | |
| } elseif ( isset( $props[ $field ] ) ) { | |
| foreach ( $props[ $field ] as $val ) { | |
| $url = is_array( $val ) ? $val['value'] : $val; | |
| $filename = static::check_error( static::download_url( $url ) ); | |
| $file = array( | |
| 'name' => basename( $url ), | |
| 'tmp_name' => $filename, | |
| 'size' => filesize( $filename ), | |
| ); | |
| $desc = is_array( $val ) ? $val['alt'] : $file['name']; | |
| $att_ids[] = static::check_error( | |
| media_handle_sideload( | |
| $file, $post_id, $desc | |
| ) | |
| ); | |
| } | |
| } | |
| $att_urls = array(); | |
| foreach ( $att_ids as $id ) { | |
| $att_urls[] = wp_get_attachment_url( $id ); | |
| } | |
| add_post_meta( $post_id, 'mf2_' . $field, $att_urls, true ); | |
| } | |
| } | |
| } | |
| /** | |
| * Stores geodata in WordPress format. | |
| * | |
| * Reads from the location and checkin properties. checkin isn't an official | |
| * mf2 property yet, but OwnYourSwarm sends it: | |
| * https://ownyourswarm.p3k.io/docs#checkins | |
| * | |
| * WordPress geo data is stored in post meta: geo_address (free text), | |
| * geo_latitude, geo_longitude, and geo_public: | |
| * https://codex.wordpress.org/Geodata | |
| * It is noted that should the HTML5 style geolocation properties of altitude, accuracy, speed, and heading are | |
| * used they would use the same geo prefix. Simple Location stores these when available using accuracy to estimate | |
| * map zoom when displayed. | |
| */ | |
| public static function store_geodata( $args ) { | |
| $properties = static::get( static::$input, 'properties' ); | |
| $location = static::get( $properties, 'location', static::get( $properties, 'checkin' ) ); | |
| $location = static::get( $location, 0, null ); | |
| // Location-visibility is an experimental property https://indieweb.org/Micropub-extensions#Location_Visibility | |
| // It attempts to mimic the geo_public property | |
| $visibility = static::get( $properties, 'location-visibility', null ); | |
| if ( $visibility ) { | |
| $visibility = array_pop( $visibility ); | |
| if ( ! isset( $args['meta_input'] ) ) { | |
| $args['meta_input'] = array(); | |
| } | |
| switch ( $visibility ) { | |
| // Currently supported by https://github.com/dshanske/simple-location as part of the Geodata store noted in codex link above | |
| // Public indicates coordinates, map, and textual description displayed | |
| case 'public': | |
| $args['meta_input']['geo_public'] = 1; | |
| break; | |
| // Private indicates no display | |
| case 'private': | |
| $args['meta_input']['geo_public'] = 0; | |
| break; | |
| // Protected which is not in the original geodata spec is used by Simple Location to indicate textual description only | |
| case 'protected': | |
| $args['meta_input']['geo_public'] = 2; | |
| break; | |
| default: | |
| static::error( 400, 'unsupported location visibility ' . $visibility ); | |
| } | |
| } | |
| if ( $location ) { | |
| if ( ! isset( $args['meta_input'] ) ) { | |
| $args['meta_input'] = array(); | |
| } | |
| if ( is_array( $location ) ) { | |
| $props = $location['properties']; | |
| if ( isset( $props['geo'] ) ) { | |
| $args['meta_input']['geo_address'] = $props['label'][0]; | |
| $props = $props['geo'][0]['properties']; | |
| } else { | |
| $parts = array( | |
| $props['name'][0], | |
| $props['street-address'][0], | |
| $props['locality'][0], | |
| $props['region'][0], | |
| $props['postal-code'][0], | |
| $props['country-name'][0], | |
| ); | |
| $args['meta_input']['geo_address'] = implode( | |
| ', ', array_filter( | |
| $parts, function( $v ) { | |
| return $v; | |
| } | |
| ) | |
| ); | |
| } | |
| $args['meta_input']['geo_latitude'] = $props['latitude'][0]; | |
| $args['meta_input']['geo_longitude'] = $props['longitude'][0]; | |
| $args['meta_input']['geo_altitude'] = $props['altitude'][0]; | |
| } elseif ( 'geo:' === substr( $location, 0, 4 ) ) { | |
| // Geo URI format: | |
| // http://en.wikipedia.org/wiki/Geo_URI#Example | |
| // https://indieweb.org/Micropub#h-entry | |
| // | |
| // e.g. geo:37.786971,-122.399677;u=35 | |
| $geo = explode( ':', substr( urldecode( $location ), 4 ) ); | |
| $geo = explode( ';', $geo[0] ); | |
| // Store the accuracy/uncertainty | |
| $args['meta_input']['geo_accuracy'] = substr( $geo[1], 2 ); | |
| $coords = explode( ',', $geo[0] ); | |
| $args['meta_input']['geo_latitude'] = trim( $coords[0] ); | |
| $args['meta_input']['geo_longitude'] = trim( $coords[1] ); | |
| // Geo URI optionally allows for altitude to be stored as a third csv | |
| if ( isset( $coords[2] ) ) { | |
| $args['meta_input']['geo_altitude'] = trim( $coords[2] ); | |
| } | |
| } elseif ( 'http' !== substr( $location, 0, 4 ) ) { | |
| $args['meta_input']['geo_address'] = $location; | |
| } | |
| } | |
| return $args; | |
| } | |
| /** | |
| * Store the return of the authorization endpoint as post metadata. Details: | |
| * https://tokens.indieauth.com/ | |
| */ | |
| public static function store_micropub_auth_response( $args ) { | |
| $micropub_auth_response = static::$micropub_auth_response; | |
| if ( $micropub_auth_response || ( is_assoc_array( $micropub_auth_response ) ) ) { | |
| $args['meta_input'] = self::get( $args, 'meta_input' ); | |
| $args['meta_input']['micropub_auth_response'] = $micropub_auth_response; | |
| } | |
| return $args; | |
| } | |
| /** | |
| * Store properties as post metadata. Details: | |
| * https://indiewebcamp.com/WordPress_Data#Microformats_data | |
| * | |
| * Uses $input, so load_input() must be called before this. | |
| * | |
| * If the request is a create, this populates $args['meta_input']. If the | |
| * request is an update, it changes the post meta values in the db directly. | |
| */ | |
| public static function store_mf2( $args ) { | |
| $props = static::$input['properties']; | |
| if ( ! isset( $args['ID'] ) && $props ) { | |
| $args['meta_input'] = self::get( $args, 'meta_input' ); | |
| $type = static::$input['type']; | |
| if ( $type ) { | |
| $args['meta_input']['mf2_type'] = $type; | |
| } | |
| foreach ( $props as $key => $val ) { | |
| $args['meta_input'][ 'mf2_' . $key ] = $val; | |
| } | |
| return $args; | |
| } | |
| $replace = static::get( static::$input, 'replace', null ); | |
| if ( $replace ) { | |
| foreach ( $replace as $prop => $val ) { | |
| update_post_meta( $args['ID'], 'mf2_' . $prop, $val ); | |
| } | |
| } | |
| $meta = get_post_meta( $args['ID'] ); | |
| $add = static::get( static::$input, 'add', null ); | |
| if ( $add ) { | |
| foreach ( $add as $prop => $val ) { | |
| $key = 'mf2_' . $prop; | |
| $cur = $meta[ $key ][0] ? unserialize( $meta[ $key ][0] ) : array(); | |
| update_post_meta( $args['ID'], $key, array_merge( $cur, $val ) ); | |
| } | |
| } | |
| $delete = static::get( static::$input, 'delete', null ); | |
| if ( $delete ) { | |
| if ( is_assoc_array( $delete ) ) { | |
| foreach ( $delete as $prop => $to_delete ) { | |
| $key = 'mf2_' . $prop; | |
| if ( isset( $meta[ $key ] ) ) { | |
| $existing = unserialize( $meta[ $key ][0] ); | |
| update_post_meta( | |
| $args['ID'], $key, | |
| array_diff( $existing, $to_delete ) | |
| ); | |
| } | |
| } | |
| } else { | |
| foreach ( $delete as $_ => $prop ) { | |
| delete_post_meta( $args['ID'], 'mf2_' . $prop ); | |
| if ( 'location' === $prop ) { | |
| delete_post_meta( $args['ID'], 'geo_latitude' ); | |
| delete_post_meta( $args['ID'], 'geo_longitude' ); | |
| } | |
| } | |
| } | |
| } | |
| return $args; | |
| } | |
| /** | |
| * Returns the mf2 properties for a post. | |
| */ | |
| public static function get_mf2( $post_id ) { | |
| $mf2 = array(); | |
| foreach ( get_post_meta( $post_id ) as $field => $val ) { | |
| $val = unserialize( $val[0] ); | |
| if ( 'mf2_type' === $field ) { | |
| $mf2['type'] = $val; | |
| } elseif ( 'mf2_' === substr( $field, 0, 4 ) ) { | |
| $mf2['properties'][ substr( $field, 4 ) ] = $val; | |
| } | |
| } | |
| return $mf2; | |
| } | |
| public static function error( $code, $message ) { | |
| static::respond( | |
| $code, array( | |
| 'error' => ( 403 === $code ) ? 'forbidden' : | |
| ( 401 === $code ) ? 'insufficient_scope' : | |
| 'invalid_request', | |
| 'error_description' => $message, | |
| ) | |
| ); | |
| } | |
| private static function check_error( $result ) { | |
| if ( ! $result ) { | |
| static::error( 400, $result ); | |
| } elseif ( is_wp_error( $result ) ) { | |
| static::error( 400, $result->get_error_message() ); | |
| } | |
| return $result; | |
| } | |
| /** | |
| * The micropub autodicovery meta tags | |
| */ | |
| public static function micropub_html_header() { | |
| printf( '<link rel="micropub" href="%s" />' . PHP_EOL, site_url( '?micropub=endpoint' ) ); | |
| } | |
| public static function indieauth_html_header() { | |
| printf( '<link rel="authorization_endpoint" href="%s" />' . PHP_EOL, get_option( 'indieauth_authorization_endpoint', MICROPUB_AUTHENTICATION_ENDPOINT ) ); | |
| printf( '<link rel="token_endpoint" href="%s" />' . PHP_EOL, get_option( 'indieauth_token_endpoint', MICROPUB_TOKEN_ENDPOINT ) ); | |
| } | |
| /** | |
| * The micropub autodicovery http-header | |
| */ | |
| public static function micropub_http_header() { | |
| static::header( 'Link', '<' . site_url( '?micropub=endpoint' ) . '>; rel="micropub"' ); | |
| } | |
| public static function indieauth_http_header() { | |
| static::header( 'Link', '<' . get_option( 'indieauth_authorization_endpoint', MICROPUB_AUTHENTICATION_ENDPOINT ) . '>; rel="authorization_endpoint"' ); | |
| static::header( 'Link', '<' . get_option( 'indieauth_token_endpoint', MICROPUB_TOKEN_ENDPOINT ) . '>; rel="token_endpoint"' ); | |
| } | |
| /** | |
| * Generates webfinger/host-meta links | |
| */ | |
| public static function micropub_jrd_links( $array ) { | |
| $array['links'][] = array( | |
| 'rel' => 'micropub', | |
| 'href' => site_url( '?micropub=endpoint' ), | |
| ); | |
| return $array; | |
| } | |
| public static function indieauth_jrd_links( $array ) { | |
| $array['links'][] = array( | |
| 'rel' => 'authorization_endpoint', | |
| 'href' => get_option( 'indieauth_authorization_endpoint', MICROPUB_AUTHENTICATION_ENDPOINT ), | |
| ); | |
| $array['links'][] = array( | |
| 'rel' => 'token_endpoint', | |
| 'href' => get_option( 'indieauth_token_endpoint', MICROPUB_TOKEN_ENDPOINT ), | |
| ); | |
| return $array; | |
| } | |
| protected static function load_input() { | |
| $content_type = explode( ';', static::get_header( 'Content-Type' ) ); | |
| $content_type = $content_type[0]; | |
| if ( 'GET' === $_SERVER['REQUEST_METHOD'] ) { | |
| static::$input = $_GET; | |
| } elseif ( 'application/json' === $content_type ) { | |
| static::$input = json_decode( static::read_input(), true ); | |
| } elseif ( ! $content_type || | |
| 'application/x-www-form-urlencoded' === $content_type || | |
| 'multipart/form-data' === $content_type ) { | |
| static::$input = array(); | |
| foreach ( $_POST as $key => $val ) { | |
| if ( 'action' === $key || 'url' === $key ) { | |
| static::$input[ $key ] = $val; | |
| } elseif ( 'h' === $key ) { | |
| static::$input['type'] = array( 'h-' . $val ); | |
| } elseif ( 'access_token' === $key ) { | |
| continue; | |
| } else { | |
| static::$input['properties'] = self::get( static::$input, 'properties' ); | |
| static::$input['properties'][ $key ] = | |
| ( is_array( $val ) && ! is_assoc_array( $val ) ) | |
| ? $val : array( $val ); | |
| } | |
| } | |
| } else { | |
| static::error( 400, 'unsupported content type ' . $content_type ); | |
| } | |
| } | |
| /** Wrappers for WordPress/PHP functions so we can mock them for unit tests. | |
| **/ | |
| protected static function read_input() { | |
| return file_get_contents( 'php://input' ); | |
| } | |
| protected static function respond( $code, $resp = null, $args = null ) { | |
| status_header( $code ); | |
| static::header( 'Content-Type', 'application/json' ); | |
| exit( $resp ? wp_json_encode( $resp ) : '' ); | |
| } | |
| public static function header( $header, $value ) { | |
| header( $header . ': ' . $value, false ); | |
| } | |
| protected static function get_header( $name ) { | |
| if ( ! static::$request_headers ) { | |
| $headers = getallheaders(); | |
| static::$request_headers = array(); | |
| foreach ( $headers as $key => $value ) { | |
| static::$request_headers[ strtolower( $key ) ] = $value; | |
| } | |
| } | |
| return static::$request_headers[ strtolower( $name ) ]; | |
| } | |
| protected static function download_url( $url ) { | |
| return static::check_error( download_url( $url ) ); | |
| } | |
| } | |
| function is_assoc_array( $array ) { | |
| return is_array( $array ) && array_values( $array ) !== $array; | |
| } | |
| // blatantly stolen from https://github.com/idno/Known/blob/master/Idno/Pages/File/View.php#L25 | |
| if ( ! function_exists( 'getallheaders' ) ) { | |
| function getallheaders() { | |
| $headers = array(); | |
| foreach ( $_SERVER as $name => $value ) { | |
| if ( 'HTTP_' === substr( $name, 0, 5 ) ) { | |
| $headers[ str_replace( ' ', '-', strtolower( str_replace( '_', ' ', substr( $name, 5 ) ) ) ) ] = $value; | |
| } elseif ( 'CONTENT_TYPE' === $name ) { | |
| $headers['content-type'] = $value; | |
| } elseif ( 'CONTENT_LENGTH' === $name ) { | |
| $headers['content-length'] = $value; | |
| } | |
| } | |
| return $headers; | |
| } | |
| } | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment