├── README.md ├── assets ├── banner-1544x500.png ├── banner-772x250.png ├── icon-128x128.png ├── icon-256x256.png └── screenshot-1.png ├── composer.json ├── frontity-embedded.php ├── includes ├── admin-page.php ├── capability-tokens.php ├── error.php ├── php-jwt │ ├── BeforeValidException.php │ ├── ExpiredException.php │ ├── JWK.php │ ├── JWT.php │ └── SignatureInvalidException.php └── template.php ├── package.json └── readme.txt /README.md: -------------------------------------------------------------------------------- 1 | # Frontity - Embedded Mode 2 | 3 | For a full explanation please refer to the [Embedded mode documentation](https://api.frontity.org/frontity-plugins/embedded-mode). If you have any questions related to the plugin feel free to share them in [Frontity community forum](https://community.frontity.org/). 4 | 5 | ## Install 6 | 7 | 1. First of all you have to install the plugin. [Download the plugin from GitHub](https://github.com/frontity/frontity-embedded/archive/refs/heads/master.zip) and upload it to your web server. For a more detailed explanation, WordPress explains how to do this [on this guide](https://wordpress.org/support/article/managing-plugins/#manual-plugin-installation). 8 | 2. Once installed, you have to activate it, go to Settings -> Frontity Embedded Mode, and define the proper Frontity Server Url. and it will be running! 9 | 10 | > Revisions are necessary to do post previews, so make sure those are activated for both your posts, pages and any custom post types. 11 | 12 | ## Environment Variables 13 | 14 | You can also use an environment variable instead of changing the URL in the plugin interface. 15 | 16 | ```php 17 | > FRONTITY_SERVER=https://myfrontityserver.com 18 | ``` 19 | 20 | ## WordPress Constant 21 | 22 | Lastly a PHP constant can be defined. This would usually be done in the wp-config.php file. 23 | 24 | ``` 25 | define( 'FRONTITY_SERVER', 'https://myfrontityserver.com' ); 26 | ``` 27 | 28 | Note that if the PHP constant exists, it takes precedence over both the environment variable and the settings page setting. 29 | 30 | ## Static Assets 31 | 32 | If you need, you can change the folder or URL where your static assets are stored using the `--public-path` option of the `npx frontity build` command. 33 | 34 | ### Example: Frontity in the same WordPress server 35 | 36 | If you are using the same server for both WordPress and Frontity, and your Frontity project is at `/wp-content/frontity`, your static folder will be at `https://yourdomain.com/wp-content/frontity/build/static`. 37 | 38 | You can change the folder when you run `npx frontity build` like this: 39 | 40 | ```bash 41 | > npx frontity build --public-path /wp-content/frontity/build/static 42 | ``` 43 | 44 | ### Example: Frontity in an external server 45 | 46 | If you deployed Frontity in an external hosting service, like for example Vercel, you can use the `--public-path` option to point directly to the Vercel URL: 47 | 48 | ```bash 49 | > npx frontity build --public-path https://myfrontityapp.now.sh/static 50 | ``` 51 | 52 | ## Local Development 53 | 54 | If you want to use the Embedded mode while you are developing in your local environment, you can do so by using the `--public-path` parameter in the `npx frontity dev` command as well: 55 | 56 | ```bash 57 | > npx frontity dev --public-path http://localhost:3000/static 58 | ``` 59 | 60 | --- 61 | 62 | ## Frontity Channels 🌎 63 | 64 | [![Community Forum Topics](https://img.shields.io/discourse/topics?color=blue&label=community%20forum&server=https%3A%2F%2Fcommunity.frontity.org%2F)](https://community.frontity.org/) [![Twitter: frontity](https://img.shields.io/twitter/follow/frontity.svg?style=social)](https://twitter.com/frontity) ![Frontity Github Stars](https://img.shields.io/github/stars/frontity/frontity?style=social) 65 | 66 | We have different channels at your disposal where you can find information about the Frontity project, discuss it and get involved: 67 | 68 | - 📖 **[Docs](https://docs.frontity.org)**: this is the place to learn how to build amazing sites with Frontity. 69 | - 👨‍👩‍👧‍👦 **[Community](https://community.frontity.org/)**: use our forum to [ask any questions](https://community.frontity.org/c/dev-talk-questions), feedback and meet great people. This is your place too to share [what are you building with Frontity](https://community.frontity.org/c/community/showcases/19)! 70 | - 🐞 **[GitHub](https://github.com/frontity)**: we use GitHub for bugs and pull requests. Questions are answered in the [community forum](https://community.frontity.org/)! 71 | - 🗣 **Social media**: a more informal place to interact with Frontity users, reach out to us on [Twitter](https://twitter.com/frontity). 72 | - 💌 **Newsletter**: do you want to receive the latest framework updates and news? Subscribe [here](https://frontity.org/) 73 | 74 | ### » Get involved 🤗 75 | 76 | Got questions or feedback about Frontity? We'd love to hear from you. Use our [community forum](https://community.frontity.org) yo ! ❤️ 77 | 78 | Frontity also welcomes contributions. There are many ways to support the project! If you don't know where to start, this guide might help: [How to contribute?](https://docs.frontity.org/contributing/how-to-contribute) 79 | -------------------------------------------------------------------------------- /assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontity/frontity-embedded/6941adef1f3c45095ba39b359379e0c8a395ab9f/assets/banner-1544x500.png -------------------------------------------------------------------------------- /assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontity/frontity-embedded/6941adef1f3c45095ba39b359379e0c8a395ab9f/assets/banner-772x250.png -------------------------------------------------------------------------------- /assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontity/frontity-embedded/6941adef1f3c45095ba39b359379e0c8a395ab9f/assets/icon-128x128.png -------------------------------------------------------------------------------- /assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontity/frontity-embedded/6941adef1f3c45095ba39b359379e0c8a395ab9f/assets/icon-256x256.png -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontity/frontity-embedded/6941adef1f3c45095ba39b359379e0c8a395ab9f/assets/screenshot-1.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontity/frontity-embedded", 3 | "description": "Embedded Mode plugin for Frontity sites", 4 | "type": "wordpress-plugin", 5 | "license": "GPLv2", 6 | "authors": [ 7 | { 8 | "name": "Frontity", 9 | "homepage": "https://frontity.org" 10 | } 11 | ], 12 | "require": {} 13 | } 14 | -------------------------------------------------------------------------------- /frontity-embedded.php: -------------------------------------------------------------------------------- 1 | '' 64 | ) ); 65 | } 66 | register_activation_hook( __FILE__, 'frontity_embedded_activate' ); 67 | 68 | /** 69 | * Delete settings on uninstall. 70 | */ 71 | function frontity_embedded_uninstall() { 72 | delete_option( 'frontity_embedded_plugin_settings' ); 73 | } 74 | register_uninstall_hook( __FILE__, 'frontity_embedded_uninstall' ); 75 | -------------------------------------------------------------------------------- /includes/admin-page.php: -------------------------------------------------------------------------------- 1 | 16 |
17 |

Frontity Embedded Mode

18 |

For detailed information and usage instructions please refer to the documentation page.

19 |
20 | 24 | 25 |
26 |
27 | 'frontity_embedded_validate_settings' 36 | ) 37 | ); 38 | 39 | add_settings_section( 40 | 'frontity_embedded_plugin_section', 41 | '', 42 | '__return_empty_string', 43 | 'frontity_embedded_plugin_page' 44 | ); 45 | 46 | add_settings_field( 47 | 'frontity_server', 48 | 'Frontity Server URL', 49 | 'frontity_embedded_frontity_server_input', 50 | 'frontity_embedded_plugin_page', 51 | 'frontity_embedded_plugin_section' 52 | ); 53 | } 54 | add_action( 'admin_init', 'frontity_embedded_register_settings' ); 55 | 56 | function frontity_embedded_validate_settings( $input ) { 57 | $output['frontity_server'] = esc_url( sanitize_text_field( $input['frontity_server'] ) ); 58 | return $output; 59 | } 60 | 61 | function frontity_embedded_frontity_server_input() { 62 | $options = get_option( 'frontity_embedded_plugin_settings' ); 63 | printf( 64 | '', 65 | esc_attr( 'frontity_embedded_plugin_settings[frontity_server]' ), 66 | esc_attr( $options['frontity_server'] ) 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /includes/capability-tokens.php: -------------------------------------------------------------------------------- 1 | $issued_at, 35 | // Expire the token in 60 seconds. 36 | "exp" => $issued_at + MINUTE_IN_SECONDS, 37 | 'type' => $payload['type'], 38 | 'post_type' => $payload['post_type'], 39 | 'post_id' => $payload['post_id'], 40 | 'generator' => 'frontity' 41 | ); 42 | 43 | return JWT::encode($payload, Frontity_Embedded_Capability_Tokens::get_private_key()); 44 | } 45 | 46 | /** 47 | * Validate a token using args. 48 | */ 49 | public static function check_capability( $args ) { 50 | $payload = (array) Frontity_Embedded_Capability_Tokens::$payload; 51 | 52 | // Do not authenticate if the token was not generated by Frontity. 53 | if ( $payload['generator'] !== 'frontity' ) return false; 54 | // Do not authenticate if the token is not for the preview. 55 | if ( $payload['type'] !== 'preview' ) return false; 56 | // Prevent using the token for requests that are not GET. 57 | if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] !== "GET" ) return false; 58 | // Prevent using the token for requests that are GET, but use the 59 | // `_method` query to override the method. 60 | if ( isset( $_GET['_method'] ) && $_GET[ '_method' ] !== "GET" ) return false; 61 | // Prevent using the token for requests that are GET, but use the 62 | // `X-HTTP-Method-Override` header to override the method. 63 | if ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) && $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] !== "GET" ) return false; 64 | 65 | $post_id = $payload['post_id']; 66 | $post_type = $payload['post_type']; 67 | 68 | // Allowed capabilites when the token is type 'preview'. You also need to 69 | // have permission to 'edit_post' or 'delete_post' for preview posts. 70 | $capabilities = array( 71 | 'read_post' => $post_id, 72 | 'edit_post' => $post_id, 73 | 'delete_post' => $post_id, 74 | ); 75 | 76 | // Prior to WordPress 5.5.1, capabilities should be specified with `page` 77 | // for pages, so we are adding them as well to support older versions of 78 | // WordPress. 79 | if ( $post_type === 'page' ) { 80 | $capabilities = array_merge( $capabilities, array( 81 | "read_page" => $id, 82 | "edit_page" => $id, 83 | "delete_page" => $id, 84 | ) ); 85 | } 86 | 87 | // Use key-value to check capabilities with an associated ID. 88 | if ( count( $args ) === 3 ) { 89 | // Get capability and ID. 90 | list( $cap, $_, $id ) = $args; 91 | // Find that capability in the capabilities array. 92 | return isset( $capabilities[ $cap ] ) && $capabilities[ $cap ] === $id; 93 | } 94 | 95 | // If it is a global capability, check if it is included as value. 96 | list( $cap ) = $args; 97 | return in_array( $cap, $capabilities ); 98 | } 99 | 100 | /** 101 | * Modify user capabilities on run time. 102 | */ 103 | public static function user_has_cap( $allcaps, $caps, $args, $user ) { 104 | // Add capability if it is allowed in the token. 105 | if ( Frontity_Embedded_Capability_Tokens::check_capability( $args )) { 106 | foreach ( $caps as $cap ) { 107 | $allcaps[ $cap ] = true; 108 | } 109 | } 110 | 111 | // Return capabilities. 112 | return $allcaps; 113 | } 114 | 115 | /** 116 | * Return the private key used to encode and decode tokens. 117 | */ 118 | private static function get_private_key() { 119 | if ( defined( 'FRONTITY_JWT_AUTH_KEY' ) ) 120 | return FRONTITY_JWT_AUTH_KEY; 121 | 122 | if ( defined( 'SECURE_AUTH_KEY' ) ) 123 | return SECURE_AUTH_KEY; 124 | 125 | // No secure auth key found. Throw an error. 126 | $error = new WP_Error( 127 | 'no-secure-auth-key', 128 | 'Please define either SECURE_AUTH_KEY or FRONTITY_JWT_AUTH_KEY in your wp-config.php file.' 129 | ); 130 | throw new Exception( $error->get_error_message() ); 131 | } 132 | 133 | /** 134 | * Decode capability tokens if present. 135 | */ 136 | private static function get_payload_from_token() { 137 | // Get HTTP Authorization Header. 138 | $header = isset( $_SERVER['HTTP_AUTHORIZATION'] ) 139 | ? sanitize_text_field( $_SERVER['HTTP_AUTHORIZATION'] ) 140 | : false; 141 | 142 | // Check for alternative header. 143 | if ( ! $header && isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) { 144 | $header = sanitize_text_field( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ); 145 | } 146 | 147 | // No Authorization Header is present. 148 | if ( ! $header ) return null; 149 | 150 | // Get and parse the token. 151 | try { 152 | list( $token ) = sscanf( $header, 'Bearer %s' ); 153 | $payload = JWT::decode( 154 | $token, 155 | Frontity_Embedded_Capability_Tokens::get_private_key(), 156 | array('HS256') 157 | ); 158 | } catch (Exception $e) { 159 | // Token is not valid. 160 | return null; 161 | } 162 | 163 | // Return the parsed token. 164 | return $payload; 165 | } 166 | } -------------------------------------------------------------------------------- /includes/error.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <?php echo get_bloginfo( 'name' ); ?> - Server Error 5 | "; 9 | } 10 | ?> 11 | 49 | 50 | 51 |
52 | " . get_bloginfo( 'name' ) . ""; 58 | } 59 | ?> 60 | There was a problem loading this page. Try again or come back later.

"; 63 | } else { 64 | echo "

Please enter a valid Frontity Server URL.

"; 65 | } 66 | ?> 67 |
68 | 69 | -------------------------------------------------------------------------------- /includes/php-jwt/BeforeValidException.php: -------------------------------------------------------------------------------- 1 | 17 | * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD 18 | * @link https://github.com/firebase/php-jwt 19 | */ 20 | class JWK 21 | { 22 | /** 23 | * Parse a set of JWK keys 24 | * 25 | * @param array $jwks The JSON Web Key Set as an associative array 26 | * 27 | * @return array An associative array that represents the set of keys 28 | * 29 | * @throws InvalidArgumentException Provided JWK Set is empty 30 | * @throws UnexpectedValueException Provided JWK Set was invalid 31 | * @throws DomainException OpenSSL failure 32 | * 33 | * @uses parseKey 34 | */ 35 | public static function parseKeySet(array $jwks) 36 | { 37 | $keys = array(); 38 | 39 | if (!isset($jwks['keys'])) { 40 | throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); 41 | } 42 | if (empty($jwks['keys'])) { 43 | throw new InvalidArgumentException('JWK Set did not contain any keys'); 44 | } 45 | 46 | foreach ($jwks['keys'] as $k => $v) { 47 | $kid = isset($v['kid']) ? $v['kid'] : $k; 48 | if ($key = self::parseKey($v)) { 49 | $keys[$kid] = $key; 50 | } 51 | } 52 | 53 | if (0 === \count($keys)) { 54 | throw new UnexpectedValueException('No supported algorithms found in JWK Set'); 55 | } 56 | 57 | return $keys; 58 | } 59 | 60 | /** 61 | * Parse a JWK key 62 | * 63 | * @param array $jwk An individual JWK 64 | * 65 | * @return resource|array An associative array that represents the key 66 | * 67 | * @throws InvalidArgumentException Provided JWK is empty 68 | * @throws UnexpectedValueException Provided JWK was invalid 69 | * @throws DomainException OpenSSL failure 70 | * 71 | * @uses createPemFromModulusAndExponent 72 | */ 73 | private static function parseKey(array $jwk) 74 | { 75 | if (empty($jwk)) { 76 | throw new InvalidArgumentException('JWK must not be empty'); 77 | } 78 | if (!isset($jwk['kty'])) { 79 | throw new UnexpectedValueException('JWK must contain a "kty" parameter'); 80 | } 81 | 82 | switch ($jwk['kty']) { 83 | case 'RSA': 84 | if (\array_key_exists('d', $jwk)) { 85 | throw new UnexpectedValueException('RSA private keys are not supported'); 86 | } 87 | if (!isset($jwk['n']) || !isset($jwk['e'])) { 88 | throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); 89 | } 90 | 91 | $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); 92 | $publicKey = \openssl_pkey_get_public($pem); 93 | if (false === $publicKey) { 94 | throw new DomainException( 95 | 'OpenSSL error: ' . \openssl_error_string() 96 | ); 97 | } 98 | return $publicKey; 99 | default: 100 | // Currently only RSA is supported 101 | break; 102 | } 103 | } 104 | 105 | /** 106 | * Create a public key represented in PEM format from RSA modulus and exponent information 107 | * 108 | * @param string $n The RSA modulus encoded in Base64 109 | * @param string $e The RSA exponent encoded in Base64 110 | * 111 | * @return string The RSA public key represented in PEM format 112 | * 113 | * @uses encodeLength 114 | */ 115 | private static function createPemFromModulusAndExponent($n, $e) 116 | { 117 | $modulus = JWT::urlsafeB64Decode($n); 118 | $publicExponent = JWT::urlsafeB64Decode($e); 119 | 120 | $components = array( 121 | 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), 122 | 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) 123 | ); 124 | 125 | $rsaPublicKey = \pack( 126 | 'Ca*a*a*', 127 | 48, 128 | self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])), 129 | $components['modulus'], 130 | $components['publicExponent'] 131 | ); 132 | 133 | // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. 134 | $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA 135 | $rsaPublicKey = \chr(0) . $rsaPublicKey; 136 | $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; 137 | 138 | $rsaPublicKey = \pack( 139 | 'Ca*a*', 140 | 48, 141 | self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), 142 | $rsaOID . $rsaPublicKey 143 | ); 144 | 145 | $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . 146 | \chunk_split(\base64_encode($rsaPublicKey), 64) . 147 | '-----END PUBLIC KEY-----'; 148 | 149 | return $rsaPublicKey; 150 | } 151 | 152 | /** 153 | * DER-encode the length 154 | * 155 | * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See 156 | * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. 157 | * 158 | * @param int $length 159 | * @return string 160 | */ 161 | private static function encodeLength($length) 162 | { 163 | if ($length <= 0x7F) { 164 | return \chr($length); 165 | } 166 | 167 | $temp = \ltrim(\pack('N', $length), \chr(0)); 168 | 169 | return \pack('Ca*', 0x80 | \strlen($temp), $temp); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /includes/php-jwt/JWT.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Anant Narayanan 20 | * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD 21 | * @link https://github.com/firebase/php-jwt 22 | */ 23 | class JWT 24 | { 25 | const ASN1_INTEGER = 0x02; 26 | const ASN1_SEQUENCE = 0x10; 27 | const ASN1_BIT_STRING = 0x03; 28 | 29 | /** 30 | * When checking nbf, iat or expiration times, 31 | * we want to provide some extra leeway time to 32 | * account for clock skew. 33 | */ 34 | public static $leeway = 0; 35 | 36 | /** 37 | * Allow the current timestamp to be specified. 38 | * Useful for fixing a value within unit testing. 39 | * 40 | * Will default to PHP time() value if null. 41 | */ 42 | public static $timestamp = null; 43 | 44 | public static $supported_algs = array( 45 | 'ES256' => array('openssl', 'SHA256'), 46 | 'HS256' => array('hash_hmac', 'SHA256'), 47 | 'HS384' => array('hash_hmac', 'SHA384'), 48 | 'HS512' => array('hash_hmac', 'SHA512'), 49 | 'RS256' => array('openssl', 'SHA256'), 50 | 'RS384' => array('openssl', 'SHA384'), 51 | 'RS512' => array('openssl', 'SHA512'), 52 | ); 53 | 54 | /** 55 | * Decodes a JWT string into a PHP object. 56 | * 57 | * @param string $jwt The JWT 58 | * @param string|array|resource $key The key, or map of keys. 59 | * If the algorithm used is asymmetric, this is the public key 60 | * @param array $allowed_algs List of supported verification algorithms 61 | * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' 62 | * 63 | * @return object The JWT's payload as a PHP object 64 | * 65 | * @throws UnexpectedValueException Provided JWT was invalid 66 | * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed 67 | * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' 68 | * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' 69 | * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim 70 | * 71 | * @uses jsonDecode 72 | * @uses urlsafeB64Decode 73 | */ 74 | public static function decode($jwt, $key, array $allowed_algs = array()) 75 | { 76 | $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; 77 | 78 | if (empty($key)) { 79 | throw new InvalidArgumentException('Key may not be empty'); 80 | } 81 | $tks = \explode('.', $jwt); 82 | if (\count($tks) != 3) { 83 | throw new UnexpectedValueException('Wrong number of segments'); 84 | } 85 | list($headb64, $bodyb64, $cryptob64) = $tks; 86 | if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { 87 | throw new UnexpectedValueException('Invalid header encoding'); 88 | } 89 | if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { 90 | throw new UnexpectedValueException('Invalid claims encoding'); 91 | } 92 | if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { 93 | throw new UnexpectedValueException('Invalid signature encoding'); 94 | } 95 | if (empty($header->alg)) { 96 | throw new UnexpectedValueException('Empty algorithm'); 97 | } 98 | if (empty(static::$supported_algs[$header->alg])) { 99 | throw new UnexpectedValueException('Algorithm not supported'); 100 | } 101 | if (!\in_array($header->alg, $allowed_algs)) { 102 | throw new UnexpectedValueException('Algorithm not allowed'); 103 | } 104 | if ($header->alg === 'ES256') { 105 | // OpenSSL expects an ASN.1 DER sequence for ES256 signatures 106 | $sig = self::signatureToDER($sig); 107 | } 108 | 109 | if (\is_array($key) || $key instanceof \ArrayAccess) { 110 | if (isset($header->kid)) { 111 | if (!isset($key[$header->kid])) { 112 | throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); 113 | } 114 | $key = $key[$header->kid]; 115 | } else { 116 | throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); 117 | } 118 | } 119 | 120 | // Check the signature 121 | if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { 122 | throw new SignatureInvalidException('Signature verification failed'); 123 | } 124 | 125 | // Check the nbf if it is defined. This is the time that the 126 | // token can actually be used. If it's not yet that time, abort. 127 | if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { 128 | throw new BeforeValidException( 129 | 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf) 130 | ); 131 | } 132 | 133 | // Check that this token has been created before 'now'. This prevents 134 | // using tokens that have been created for later use (and haven't 135 | // correctly used the nbf claim). 136 | if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { 137 | throw new BeforeValidException( 138 | 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) 139 | ); 140 | } 141 | 142 | // Check if this token has expired. 143 | if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { 144 | throw new ExpiredException('Expired token'); 145 | } 146 | 147 | return $payload; 148 | } 149 | 150 | /** 151 | * Converts and signs a PHP object or array into a JWT string. 152 | * 153 | * @param object|array $payload PHP object or array 154 | * @param string $key The secret key. 155 | * If the algorithm used is asymmetric, this is the private key 156 | * @param string $alg The signing algorithm. 157 | * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' 158 | * @param mixed $keyId 159 | * @param array $head An array with header elements to attach 160 | * 161 | * @return string A signed JWT 162 | * 163 | * @uses jsonEncode 164 | * @uses urlsafeB64Encode 165 | */ 166 | public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) 167 | { 168 | $header = array('typ' => 'JWT', 'alg' => $alg); 169 | if ($keyId !== null) { 170 | $header['kid'] = $keyId; 171 | } 172 | if (isset($head) && \is_array($head)) { 173 | $header = \array_merge($head, $header); 174 | } 175 | $segments = array(); 176 | $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); 177 | $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); 178 | $signing_input = \implode('.', $segments); 179 | 180 | $signature = static::sign($signing_input, $key, $alg); 181 | $segments[] = static::urlsafeB64Encode($signature); 182 | 183 | return \implode('.', $segments); 184 | } 185 | 186 | /** 187 | * Sign a string with a given key and algorithm. 188 | * 189 | * @param string $msg The message to sign 190 | * @param string|resource $key The secret key 191 | * @param string $alg The signing algorithm. 192 | * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' 193 | * 194 | * @return string An encrypted message 195 | * 196 | * @throws DomainException Unsupported algorithm was specified 197 | */ 198 | public static function sign($msg, $key, $alg = 'HS256') 199 | { 200 | if (empty(static::$supported_algs[$alg])) { 201 | throw new DomainException('Algorithm not supported'); 202 | } 203 | list($function, $algorithm) = static::$supported_algs[$alg]; 204 | switch ($function) { 205 | case 'hash_hmac': 206 | return \hash_hmac($algorithm, $msg, $key, true); 207 | case 'openssl': 208 | $signature = ''; 209 | $success = \openssl_sign($msg, $signature, $key, $algorithm); 210 | if (!$success) { 211 | throw new DomainException("OpenSSL unable to sign data"); 212 | } else { 213 | if ($alg === 'ES256') { 214 | $signature = self::signatureFromDER($signature, 256); 215 | } 216 | return $signature; 217 | } 218 | } 219 | } 220 | 221 | /** 222 | * Verify a signature with the message, key and method. Not all methods 223 | * are symmetric, so we must have a separate verify and sign method. 224 | * 225 | * @param string $msg The original message (header and body) 226 | * @param string $signature The original signature 227 | * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key 228 | * @param string $alg The algorithm 229 | * 230 | * @return bool 231 | * 232 | * @throws DomainException Invalid Algorithm or OpenSSL failure 233 | */ 234 | private static function verify($msg, $signature, $key, $alg) 235 | { 236 | if (empty(static::$supported_algs[$alg])) { 237 | throw new DomainException('Algorithm not supported'); 238 | } 239 | 240 | list($function, $algorithm) = static::$supported_algs[$alg]; 241 | switch ($function) { 242 | case 'openssl': 243 | $success = \openssl_verify($msg, $signature, $key, $algorithm); 244 | if ($success === 1) { 245 | return true; 246 | } elseif ($success === 0) { 247 | return false; 248 | } 249 | // returns 1 on success, 0 on failure, -1 on error. 250 | throw new DomainException( 251 | 'OpenSSL error: ' . \openssl_error_string() 252 | ); 253 | case 'hash_hmac': 254 | default: 255 | $hash = \hash_hmac($algorithm, $msg, $key, true); 256 | if (\function_exists('hash_equals')) { 257 | return \hash_equals($signature, $hash); 258 | } 259 | $len = \min(static::safeStrlen($signature), static::safeStrlen($hash)); 260 | 261 | $status = 0; 262 | for ($i = 0; $i < $len; $i++) { 263 | $status |= (\ord($signature[$i]) ^ \ord($hash[$i])); 264 | } 265 | $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); 266 | 267 | return ($status === 0); 268 | } 269 | } 270 | 271 | /** 272 | * Decode a JSON string into a PHP object. 273 | * 274 | * @param string $input JSON string 275 | * 276 | * @return object Object representation of JSON string 277 | * 278 | * @throws DomainException Provided string was invalid JSON 279 | */ 280 | public static function jsonDecode($input) 281 | { 282 | if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { 283 | /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you 284 | * to specify that large ints (like Steam Transaction IDs) should be treated as 285 | * strings, rather than the PHP default behaviour of converting them to floats. 286 | */ 287 | $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); 288 | } else { 289 | /** Not all servers will support that, however, so for older versions we must 290 | * manually detect large ints in the JSON string and quote them (thus converting 291 | *them to strings) before decoding, hence the preg_replace() call. 292 | */ 293 | $max_int_length = \strlen((string) PHP_INT_MAX) - 1; 294 | $json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); 295 | $obj = \json_decode($json_without_bigints); 296 | } 297 | 298 | if ($errno = \json_last_error()) { 299 | static::handleJsonError($errno); 300 | } elseif ($obj === null && $input !== 'null') { 301 | throw new DomainException('Null result with non-null input'); 302 | } 303 | return $obj; 304 | } 305 | 306 | /** 307 | * Encode a PHP object into a JSON string. 308 | * 309 | * @param object|array $input A PHP object or array 310 | * 311 | * @return string JSON representation of the PHP object or array 312 | * 313 | * @throws DomainException Provided object could not be encoded to valid JSON 314 | */ 315 | public static function jsonEncode($input) 316 | { 317 | $json = \json_encode($input); 318 | if ($errno = \json_last_error()) { 319 | static::handleJsonError($errno); 320 | } elseif ($json === 'null' && $input !== null) { 321 | throw new DomainException('Null result with non-null input'); 322 | } 323 | return $json; 324 | } 325 | 326 | /** 327 | * Decode a string with URL-safe Base64. 328 | * 329 | * @param string $input A Base64 encoded string 330 | * 331 | * @return string A decoded string 332 | */ 333 | public static function urlsafeB64Decode($input) 334 | { 335 | $remainder = \strlen($input) % 4; 336 | if ($remainder) { 337 | $padlen = 4 - $remainder; 338 | $input .= \str_repeat('=', $padlen); 339 | } 340 | return \base64_decode(\strtr($input, '-_', '+/')); 341 | } 342 | 343 | /** 344 | * Encode a string with URL-safe Base64. 345 | * 346 | * @param string $input The string you want encoded 347 | * 348 | * @return string The base64 encode of what you passed in 349 | */ 350 | public static function urlsafeB64Encode($input) 351 | { 352 | return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); 353 | } 354 | 355 | /** 356 | * Helper method to create a JSON error. 357 | * 358 | * @param int $errno An error number from json_last_error() 359 | * 360 | * @return void 361 | */ 362 | private static function handleJsonError($errno) 363 | { 364 | $messages = array( 365 | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', 366 | JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', 367 | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', 368 | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', 369 | JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 370 | ); 371 | throw new DomainException( 372 | isset($messages[$errno]) 373 | ? $messages[$errno] 374 | : 'Unknown JSON error: ' . $errno 375 | ); 376 | } 377 | 378 | /** 379 | * Get the number of bytes in cryptographic strings. 380 | * 381 | * @param string $str 382 | * 383 | * @return int 384 | */ 385 | private static function safeStrlen($str) 386 | { 387 | if (\function_exists('mb_strlen')) { 388 | return \mb_strlen($str, '8bit'); 389 | } 390 | return \strlen($str); 391 | } 392 | 393 | /** 394 | * Convert an ECDSA signature to an ASN.1 DER sequence 395 | * 396 | * @param string $sig The ECDSA signature to convert 397 | * @return string The encoded DER object 398 | */ 399 | private static function signatureToDER($sig) 400 | { 401 | // Separate the signature into r-value and s-value 402 | list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2)); 403 | 404 | // Trim leading zeros 405 | $r = \ltrim($r, "\x00"); 406 | $s = \ltrim($s, "\x00"); 407 | 408 | // Convert r-value and s-value from unsigned big-endian integers to 409 | // signed two's complement 410 | if (\ord($r[0]) > 0x7f) { 411 | $r = "\x00" . $r; 412 | } 413 | if (\ord($s[0]) > 0x7f) { 414 | $s = "\x00" . $s; 415 | } 416 | 417 | return self::encodeDER( 418 | self::ASN1_SEQUENCE, 419 | self::encodeDER(self::ASN1_INTEGER, $r) . 420 | self::encodeDER(self::ASN1_INTEGER, $s) 421 | ); 422 | } 423 | 424 | /** 425 | * Encodes a value into a DER object. 426 | * 427 | * @param int $type DER tag 428 | * @param string $value the value to encode 429 | * @return string the encoded object 430 | */ 431 | private static function encodeDER($type, $value) 432 | { 433 | $tag_header = 0; 434 | if ($type === self::ASN1_SEQUENCE) { 435 | $tag_header |= 0x20; 436 | } 437 | 438 | // Type 439 | $der = \chr($tag_header | $type); 440 | 441 | // Length 442 | $der .= \chr(\strlen($value)); 443 | 444 | return $der . $value; 445 | } 446 | 447 | /** 448 | * Encodes signature from a DER object. 449 | * 450 | * @param string $der binary signature in DER format 451 | * @param int $keySize the number of bits in the key 452 | * @return string the signature 453 | */ 454 | private static function signatureFromDER($der, $keySize) 455 | { 456 | // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE 457 | list($offset, $_) = self::readDER($der); 458 | list($offset, $r) = self::readDER($der, $offset); 459 | list($offset, $s) = self::readDER($der, $offset); 460 | 461 | // Convert r-value and s-value from signed two's compliment to unsigned 462 | // big-endian integers 463 | $r = \ltrim($r, "\x00"); 464 | $s = \ltrim($s, "\x00"); 465 | 466 | // Pad out r and s so that they are $keySize bits long 467 | $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); 468 | $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); 469 | 470 | return $r . $s; 471 | } 472 | 473 | /** 474 | * Reads binary DER-encoded data and decodes into a single object 475 | * 476 | * @param string $der the binary data in DER format 477 | * @param int $offset the offset of the data stream containing the object 478 | * to decode 479 | * @return array [$offset, $data] the new offset and the decoded object 480 | */ 481 | private static function readDER($der, $offset = 0) 482 | { 483 | $pos = $offset; 484 | $size = \strlen($der); 485 | $constructed = (\ord($der[$pos]) >> 5) & 0x01; 486 | $type = \ord($der[$pos++]) & 0x1f; 487 | 488 | // Length 489 | $len = \ord($der[$pos++]); 490 | if ($len & 0x80) { 491 | $n = $len & 0x1f; 492 | $len = 0; 493 | while ($n-- && $pos < $size) { 494 | $len = ($len << 8) | \ord($der[$pos++]); 495 | } 496 | } 497 | 498 | // Value 499 | if ($type == self::ASN1_BIT_STRING) { 500 | $pos++; // Skip the first contents octet (padding indicator) 501 | $data = \substr($der, $pos, $len - 1); 502 | $pos += $len - 1; 503 | } elseif (!$constructed) { 504 | $data = \substr($der, $pos, $len); 505 | $pos += $len; 506 | } else { 507 | $data = null; 508 | } 509 | 510 | return array($pos, $data); 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /includes/php-jwt/SignatureInvalidException.php: -------------------------------------------------------------------------------- 1 | 'preview', 47 | 'post_type' => $type, 48 | 'post_id' => $id, 49 | ) 50 | ); 51 | 52 | $url = $url . '&frontity_source_auth=Bearer ' . $token; 53 | } 54 | 55 | /** 56 | * Filters the URL before doing the request. This allows external code to alter 57 | * the URL before the request is made. 58 | * 59 | * @hook frontity_embedded_request_url 60 | * @param string $url The frontity request url 61 | * @return string 62 | */ 63 | $url = apply_filters( 'frontity_embedded_request_url', $url ); 64 | 65 | /** 66 | * Filters the arguments before doing the request. This allows external code to 67 | * alter the `wp_remote_get` parameters before the request is made. 68 | * 69 | * @hook frontity_embedded_request_args 70 | * @param array $args The `wp_remote_get` args 71 | * @return array 72 | */ 73 | $args = apply_filters( 'frontity_embedded_request_args', array( 'timeout' => 30 ) ); 74 | 75 | // Do the request to the Frontity server. 76 | $response = wp_remote_get( $url, $args ); 77 | 78 | if ( is_wp_error( $response ) ) { 79 | // If the request fails, set the status to 500. 80 | status_header( 500 ); 81 | if ( WP_DEBUG === true ) { 82 | // If the WordPress is in debug mode, throw an error. 83 | throw new Exception( $response->get_error_message() ); 84 | } else { 85 | // If not, send the error page. 86 | require_once plugin_dir_path( __FILE__ ) . 'error.php'; 87 | } 88 | } else if ( substr( (string) $response[ "response" ][ "code" ], 0, 1 ) === "5" ) { 89 | // If the request didn't fail, but Frontity returned an error, set the status 90 | // to 500. 91 | status_header( 500 ); 92 | if ( isset( $response[ "headers" ][ "x-frontity-dev" ] ) ) { 93 | // If Frontity is in development mode, show the error. 94 | echo $response[ "body" ]; 95 | } else { 96 | // If not, send the error page. 97 | require_once plugin_dir_path( __FILE__ ) . 'error.php'; 98 | } 99 | } else { 100 | global $wp_query; 101 | 102 | // Consider static all kind of files Webpack returns as static. 103 | $isStatic = preg_match( 104 | '/\.(js|png|jpe?g|gif|svg|woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/i', 105 | $_SERVER['REQUEST_URI'] 106 | ); 107 | 108 | // Pass through the Content-Type header. 109 | header( 'content-type: ' . $response['headers']['content-type'] ); 110 | 111 | // Pass through the status of the response. 112 | status_header( $response['response']['code'] ); 113 | 114 | // Override is_404 of static assets. 115 | if ( $isStatic && $response['response']['code'] === 200 ) 116 | $wp_query->is_404 = false; 117 | 118 | // Add the Admin Bar. 119 | if ( !$isStatic) { 120 | // Divide the HTML to be able to insert things in the and . 121 | list($head, $rest) = preg_split('/(?=<\/head>)/', wp_remote_retrieve_body( $response ) ); 122 | list($body, $end) = preg_split('/(?=<\/body>)/', $rest); 123 | 124 | // Echo the , but don't echo tag yet. 125 | echo $head; 126 | 127 | /** 128 | * Fires a do_action hook similar to wp_head. 129 | * https://developer.wordpress.org/reference/functions/wp_head/ 130 | */ 131 | do_action('frontity_embedded_wp_head'); 132 | 133 | if (is_admin_bar_showing()) { 134 | add_action( 'admin_print_styles', 'print_emoji_styles' ); 135 | add_action( 'admin_print_styles', 'print_admin_styles', 20 ); 136 | do_action( 'admin_print_styles' ); 137 | } 138 | // Echo the , but don't echo the tag yet. 139 | echo $body; 140 | 141 | /** 142 | * Fires a do_action hook similar to wp_footer. 143 | * https://developer.wordpress.org/reference/functions/wp_footer/ 144 | */ 145 | do_action('frontity_embedded_wp_footer'); 146 | 147 | // Echo the admin bar HTML. 148 | if (is_admin_bar_showing()) { 149 | // Get the scripts and styles of the Admin Bar and echo them. 150 | add_action( 'admin_print_scripts', 'print_emoji_detection_script' ); 151 | add_action( 'admin_print_scripts', 'print_head_scripts', 20 ); 152 | do_action( 'admin_print_scripts' ); 153 | _admin_bar_bump_cb(); 154 | wp_admin_bar_header(); 155 | wp_admin_bar_render(); 156 | } 157 | 158 | // Echo the final and tags. 159 | echo $end; 160 | } else { 161 | echo wp_remote_retrieve_body( $response ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontity-embedded", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "GPLv2", 6 | "scripts": {} 7 | } 8 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Frontity Embedded Mode === 2 | Contributors: frontity 3 | Tags: rest, api, embedded, decoupled, frontity 4 | Requires at least: 5.6 5 | Tested up to: 5.9 6 | Stable tag: 1.5.1 7 | Requires PHP: 7.0 8 | License: GPLv3 9 | License URI: http://www.gnu.org/licenses/gpl.html 10 | 11 | Enables the Embedded Mode implementation of Frontity, replacing the active WordPress theme. Made by [Frontity](https://frontity.org?utm_source=plugin-repository&utm_medium=readme&utm_campaign=embedded-mode-plugin). 12 | 13 | == Description == 14 | 15 | This plugin enables an **Embedded Mode** implementation of Frontity. It can be contrasted with **Decoupled Mode**. In Decoupled Mode the primary domain points to the Frontity site, with the WordPress site being on a secondary domain or on a subdomain of the primary domain. 16 | 17 | In Embedded Mode the primary domain points to the WordPress site, and the Frontity site can be on another domain. 18 | 19 | This plugin replaces the active WordPress theme with the Frontity installation. Frontity therefore effectively becomes the WordPress site's theme. It works by substituting its own template.php in place of any call made to the WordPress template hierarchy. 20 | 21 | == Support == 22 | 23 | For a more detailed explanation, please refer to its own [documentation](https://api.frontity.org/frontity-plugins/embedded-mode). If you have any questions related to the plugin, feel free to share them in [Frontity community forum](https://community.frontity.org/) and, if you want to contribute to the code, Pull Requests are welcome on [GitHub](https://github.com/frontity/frontity-embedded). 24 | 25 | == Settings == 26 | 27 | #### Frontity server URL 28 | 29 | This plugin only has 1 setting, the URL of the Frontity Server. It can be defined in the plugin interface, with an environment variable or with a WordPress constant. 30 | 31 | == Installation == 32 | 33 | 1. First of all you have to install the plugin. You can do it: 34 | * **Automatic**: from within WordPress dashboard go to Plugins, click Add New button, search for Frontity Embedded Mode by Frontity and click Install Now. 35 | * **Manual**: this method requires to download the plugin and upload it to your web server via FTP. For a more detailed explanation, WordPress explains how to do this [on this guide](https://wordpress.org/support/article/managing-plugins/#manual-plugin-installation). 36 | 37 | 2. Once installed, you have to activate it, go to Settings -> Frontity Embedded Mode, and define the proper Frontity Server URL and it will be running! 38 | 39 | == Screenshots == 40 | 41 | 1. Frontity Embedded Mode Settings 42 | 43 | == Changelog == 44 | 45 | = 1.5.1 = 46 | 47 | - Fix Admin bar in WP 6.0 - [#30](https://github.com/frontity/frontity-embedded/pull/30) Thanks [@tobeycodes](https://github.com/tobeycodes)! 48 | 49 | = 1.5.0 = 50 | 51 | - Add `frontity_embedded_wp_head` and `frontity_embedded_wp_footer` hooks - [#26](https://github.com/frontity/frontity-embedded/pull/26) Thanks [@johnfrancisli](https://github.com/johnfrancisli)! 52 | 53 | = 1.4.2 = 54 | 55 | - Use [admin_print_scripts](https://developer.wordpress.org/reference/hooks/admin_print_scripts/) and [admin_print_styles](https://developer.wordpress.org/reference/hooks/admin_print_styles/) to echo scripts and styles in admin pages. 56 | - Change Capability_Tokens class name to Frontity_Embedded_Capability_Tokens to be more specific. 57 | - Change default setting value to an empty string and add a error message asking for the Frontity Server URL in case this is missing. 58 | - Use untrailingslashit function in the server url setting. 59 | 60 | = 1.4.1 = 61 | 62 | - Add a link to the documentation in the Admin page - [#22](https://github.com/frontity/frontity-embedded/pull/22) Thanks [@mburridge](https://github.com/mburridge)! 63 | 64 | = 1.4.0 = 65 | 66 | - Add a `compose.json` - [#21](https://github.com/frontity/frontity-embedded/pull/21) Thanks [@dsawardekar](https://github.com/dsawardekar)! 67 | - Add filters to change the request URL and arguments - [#20](https://github.com/frontity/frontity-embedded/pull/20) Thanks [@dsawardekar](https://github.com/dsawardekar)! 68 | 69 | = 1.3.0 = 70 | 71 | - Avoid using glob to load files - [#18](https://github.com/frontity/frontity-embedded/pull/18) Thanks [@SantosGuillamot](https://github.com/SantosGuillamot)! 72 | - Improve handling of errors - [#17](https://github.com/frontity/frontity-embedded/pull/17) Thanks [@SantosGuillamot](https://github.com/SantosGuillamot) and [@luisherranz](https://github.com/luisherranz)! 73 | - Restrict token capabilities - [#16](https://github.com/frontity/frontity-embedded/pull/16) Thanks [@Darerodz](https://github.com/Darerodz) and [@luisherranz](https://github.com/luisherranz)! 74 | - Add a UI for the settings - [#15](https://github.com/frontity/frontity-embedded/pull/15) Thanks [@SantosGuillamot](https://github.com/SantosGuillamot) and [@luisherranz](https://github.com/luisherranz)! 75 | 76 | = 1.2.0 = 77 | 78 | - Add `frontity_embedded` query to Frontity request - [#11](https://github.com/frontity/frontity-embedded/pull/11) Thanks [@Darerodz](https://github.com/Darerodz) and [@luisherranz](https://github.com/luisherranz)! 79 | - Increase timeout of request to the Frontity server - [#9](https://github.com/frontity/frontity-embedded/pull/9) Thanks [@luisherranz](https://github.com/luisherranz)! 80 | - Add the option of defining the server URL using a PHP constant - [#5](https://github.com/frontity/frontity-embedded/pull/5) Thanks [@Darerodz](https://github.com/Darerodz)! 81 | - Add the option of defining the server URL using a PHP constant - [#5](https://github.com/frontity/frontity-embedded/pull/5) Thanks [@Darerodz](https://github.com/Darerodz)! 82 | 83 | = 1.1.0 = 84 | 85 | - Add support for post previews - [#3](https://github.com/frontity/frontity-embedded/pull/3) Thanks [@Darerodz](https://github.com/Darerodz)! 86 | 87 | = 1.0.0 = 88 | 89 | - Release the first version of the Frontity Embedded Mode plugin - Thanks [@luisherranz](https://github.com/luisherranz)! 90 | --------------------------------------------------------------------------------