├── assets ├── pencil.png ├── screenshot-1.png └── icon-fb-login.png ├── languages ├── de_DE.mo ├── instant-articles.pot └── de_DE.po ├── .wordpress-org ├── icon-128x128.jpg ├── icon-256x256.jpg ├── banner-772x250.jpg └── banner-1544x500.jpg ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ ├── deploy.yml │ ├── stale.yml │ ├── tests.yml │ └── cs-lint.yml ├── .gitignore ├── .distignore ├── vipgo-helper.php ├── src ├── compat │ ├── comscore.js │ ├── get-the-image-rules-configuration.json │ ├── apester-rules-configuration.json │ ├── class-instant-articles-apester.php │ ├── class-instant-articles-playbuzz.php │ ├── class-instant-articles-get-the-image.php │ ├── playbuzz-rules-configuration.json │ ├── class-instant-articles-comscore.php │ ├── class-instant-articles-gtm4wp.php │ ├── class-instant-articles-co-authors-plus.php │ ├── class-instant-articles-google-analytics-for-wordpress.php │ ├── class-instant-articles-yoast-seo.php │ ├── jetpack-rules-configuration.json │ └── class-instant-articles-jetpack.php ├── meta-box │ ├── meta-box-error.php │ ├── meta-box-loader-template.php │ ├── class-instant-articles-meta-box.php │ └── meta-box-template.php ├── wizard │ ├── class-instant-articles-option-fb-page.php │ ├── class-instant-articles-option-fb-app.php │ ├── class-instant-articles-option-styles.php │ ├── templates │ │ └── advanced-template.php │ ├── class-instant-articles-wizard.php │ ├── class-instant-articles-option-amp.php │ ├── class-instant-articles-option-analytics.php │ ├── class-instant-articles-option-publishing.php │ ├── class-instant-articles-option-ads.php │ └── class-instant-articles-option.php ├── compat.php ├── class-instant-articles-signer.php ├── embeds.php └── class-instant-articles-amp-markup.php ├── css ├── instant-articles-index-column.css ├── instant-articles-meta-box.css └── instant-articles-wizard.css ├── js ├── instant-articles-option-analytics.js ├── instant-articles-option-publishing.js ├── instant-articles-meta-box.js └── instant-articles-option-ads.js ├── .editorconfig ├── phpunit.xml.dist ├── wpcom-helper.php ├── tests ├── bootstrap.php ├── InstantArticlesPostTest.php └── Facebook │ └── InstantArticles │ └── Transformer │ ├── WPTransformerTest.php │ ├── wp-ia.xml │ └── wp.html ├── feed-template.php ├── composer.json ├── .phpcs.xml.dist ├── bin └── install-wp-tests.sh ├── README.md ├── rules-configuration.json └── release.sh /assets/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/fb-instant-articles/HEAD/assets/pencil.png -------------------------------------------------------------------------------- /languages/de_DE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/fb-instant-articles/HEAD/languages/de_DE.mo -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/fb-instant-articles/HEAD/assets/screenshot-1.png -------------------------------------------------------------------------------- /assets/icon-fb-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/fb-instant-articles/HEAD/assets/icon-fb-login.png -------------------------------------------------------------------------------- /.wordpress-org/icon-128x128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/fb-instant-articles/HEAD/.wordpress-org/icon-128x128.jpg -------------------------------------------------------------------------------- /.wordpress-org/icon-256x256.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/fb-instant-articles/HEAD/.wordpress-org/icon-256x256.jpg -------------------------------------------------------------------------------- /.wordpress-org/banner-772x250.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/fb-instant-articles/HEAD/.wordpress-org/banner-772x250.jpg -------------------------------------------------------------------------------- /.wordpress-org/banner-1544x500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/fb-instant-articles/HEAD/.wordpress-org/banner-1544x500.jpg -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This PR: 2 | 3 | * [x] 4 | * [ ] 5 | * [ ] 6 | 7 | Follows # 8 | 9 | Relates to # 10 | 11 | Fixes # 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /facebook-instant-articles-wp.zip 2 | /.phpcs.xml 3 | /.phpunit.result.cache 4 | /.wp-env.override.json 5 | /composer.lock 6 | /vendor 7 | -------------------------------------------------------------------------------- /.distignore: -------------------------------------------------------------------------------- 1 | /.distignore 2 | /.editorconfig 3 | /.git 4 | /.gitignore 5 | /.github 6 | /.phpcs.xml.dist 7 | /.phpunit.result.cache 8 | /.wordpress-org 9 | /bin 10 | /composer.json 11 | /phpunit.xml.dist 12 | /release.sh 13 | /tests 14 | 15 | -------------------------------------------------------------------------------- /vipgo-helper.php: -------------------------------------------------------------------------------- 1 | 10 |

An unkown error occurred while transforming the article. Please verify your rules configuration is correct.

11 | -------------------------------------------------------------------------------- /src/meta-box/meta-box-loader-template.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /js/instant-articles-option-analytics.js: -------------------------------------------------------------------------------- 1 | jQuery( function () { 2 | var idEmbedCodeEnabled = '#' + INSTANT_ARTICLES_OPTION_ANALYTICS['option_field_id_embed_code_enabled']; 3 | var idEmbedCode = '#' + INSTANT_ARTICLES_OPTION_ANALYTICS['option_field_id_embed_code']; 4 | var $rowEmbedCode = jQuery( idEmbedCode ).parents( 'tr' ); 5 | var $embedCodeEnabled = jQuery( idEmbedCodeEnabled ); 6 | $embedCodeEnabled.change( function () { 7 | if ( $embedCodeEnabled.is( ':checked' ) ) { 8 | 9 | $rowEmbedCode.show(); 10 | 11 | } else { 12 | 13 | $rowEmbedCode.hide(); 14 | 15 | } 16 | } ).trigger( 'change' ); 17 | } ); 18 | -------------------------------------------------------------------------------- /js/instant-articles-option-publishing.js: -------------------------------------------------------------------------------- 1 | jQuery( function () { 2 | var idCustomRulesEnabled = '#' + INSTANT_ARTICLES_OPTION_PUBLISHING['option_field_id_custom_rules_enabled']; 3 | var idCustomRules = '#' + INSTANT_ARTICLES_OPTION_PUBLISHING['option_field_id_custom_rules']; 4 | var $rowCustomRules = jQuery( idCustomRules ).parents( 'tr' ); 5 | var $customRulesEnabled = jQuery( idCustomRulesEnabled ); 6 | $customRulesEnabled.change( function () { 7 | if ( $customRulesEnabled.is( ':checked' ) ) { 8 | 9 | $rowCustomRules.show(); 10 | 11 | } else { 12 | 13 | $rowCustomRules.hide(); 14 | 15 | } 16 | } ).trigger( 'change' ); 17 | } ); 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # It is based on https://core.trac.wordpress.org/browser/trunk/.editorconfig. 3 | # See https://editorconfig.org for more information about the standard. 4 | 5 | # WordPress Coding Standards 6 | # https://make.wordpress.org/core/handbook/coding-standards/ 7 | 8 | root = true 9 | 10 | [*] 11 | charset = utf-8 12 | end_of_line = lf 13 | insert_final_newline = true 14 | trim_trailing_whitespace = true 15 | indent_style = tab 16 | 17 | [*.yml] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.txt] 25 | end_of_line = crlf 26 | -------------------------------------------------------------------------------- /src/compat/apester-rules-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "class": "IgnoreRule", 5 | "selector": "//script[contains(@src,'apester')]" 6 | }, 7 | { 8 | "class": "InteractiveRule", 9 | "selector": "div.apester-media", 10 | "properties": { 11 | "interactive.iframe": { 12 | "type": "multiple", 13 | "children": [ 14 | { 15 | "type": "fragment", 16 | "fragment": "" 17 | }, 18 | { 19 | "type": "element", 20 | "selector": "div.apester-media" 21 | } 22 | ] 23 | } 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Steps required to reproduce the problem 2 | 3 | 1. 4 | 2. 5 | 3. 6 | 7 | ## Expected Result 8 | 9 | 10 | ## Actual Result 11 | If your issue is about the error message _No rules defined for ..._. Please do the following. 12 | * Go to the Wordpress editor and open the post 13 | * In the Instant Article plugin section click on _[Toggle debug information]_ 14 | * Share both _Source Markup_ and _Transformed Markup_ 15 | 16 | ## Version Info 17 | 18 | Please state _exact_ versions, not _latest_ or _new_. 19 | 20 | * Plugin version: 21 | * WordPress version: 22 | * PHP version: 23 | 24 | Without the above information we are not able to debug the issue and will close it. -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to WordPress.org 2 | on: 3 | release: 4 | types: [released] 5 | # Allow manually triggering the workflow. 6 | workflow_dispatch: 7 | jobs: 8 | release: 9 | name: New release to WordPress.org 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Build # Remove or modify this step as needed 15 | run: composer install --no-dev --classmap-authoritative 16 | - name: Push to WordPress.org 17 | uses: 10up/action-wordpress-plugin-deploy@stable 18 | env: 19 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 20 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 21 | -------------------------------------------------------------------------------- /src/compat/class-instant-articles-apester.php: -------------------------------------------------------------------------------- 1 | loadRules( $configuration ); 24 | 25 | return $transformer; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/compat/class-instant-articles-playbuzz.php: -------------------------------------------------------------------------------- 1 | loadRules( $configuration ); 24 | 25 | return $transformer; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/compat/class-instant-articles-get-the-image.php: -------------------------------------------------------------------------------- 1 | loadRules( $configuration ); 24 | 25 | return $transformer; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ./tests 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /js/instant-articles-meta-box.js: -------------------------------------------------------------------------------- 1 | function instant_articles_force_submit ( post_ID ) { 2 | var data = { 3 | 'action': 'instant_articles_force_submit', 4 | 'post_ID': post_ID, 5 | 'force': jQuery( '#instant_articles_force_submit' ).is( ':checked' ), 6 | 'security': jQuery( '#instant_articles_force_submit' ).attr( 'data-security' ) 7 | }; 8 | jQuery.post( ajaxurl, data, function(response) { 9 | instant_articles_load_meta_box( post_ID ); 10 | }); 11 | } 12 | function instant_articles_load_meta_box ( post_ID ) { 13 | var data = { 14 | 'action': 'instant_articles_meta_box', 15 | 'post_ID': post_ID 16 | }; 17 | jQuery.post( ajaxurl, data, function(response) { 18 | jQuery( '#instant_article_meta_box .inside' ).html( response ); 19 | jQuery( '#instant_articles_force_submit').click( function () { 20 | instant_articles_force_submit( post_ID ); 21 | } ); 22 | }, 'html' ); 23 | jQuery( '#instant_article_meta_box' ).delegate( '.instant-articles-toggle-debug', 'click', function () { 24 | jQuery( '#instant_article_meta_box' ).toggleClass( 'instant-articles-show-debug' ); 25 | return false; 26 | } ); 27 | } 28 | -------------------------------------------------------------------------------- /src/compat/playbuzz-rules-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "class": "IgnoreRule", 5 | "selector": "//p/script[contains(@src,'playbuzz')]" 6 | }, 7 | { 8 | "class": "InteractiveRule", 9 | "selector": "div.pb_feed", 10 | "properties": { 11 | "interactive.iframe": { 12 | "type": "multiple", 13 | "children": [ 14 | { 15 | "type": "fragment", 16 | "fragment": "" 17 | }, 18 | { 19 | "type": "element", 20 | "selector": "div.pb_feed" 21 | } 22 | ] 23 | } 24 | } 25 | }, 26 | { 27 | "class": "InteractiveRule", 28 | "selector": "div.playbuzz", 29 | "properties": { 30 | "interactive.iframe": { 31 | "type": "multiple", 32 | "children": [ 33 | { 34 | "type": "fragment", 35 | "fragment": "" 36 | }, 37 | { 38 | "type": "element", 39 | "selector": "div.playbuzz" 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/wizard/class-instant-articles-option-fb-page.php: -------------------------------------------------------------------------------- 1 | 'Facebook Page', 19 | 'description' => '

Follow these instructions to sign up to Instant Articles and get your Facebook Page ID.

', 20 | ); 21 | 22 | public static $fields = array( 23 | 24 | 'page_id' => array( 25 | 'visible' => true, 26 | 'label' => 'Facebook Page ID', 27 | 'default' => '', 28 | 'description' => 'Fill in your Facebook Page ID.' 29 | ), 30 | 31 | ); 32 | 33 | /** 34 | * Constructor. 35 | * 36 | * @since 0.4 37 | */ 38 | public function __construct() { 39 | parent::__construct( 40 | self::OPTION_KEY, 41 | self::$sections, 42 | self::$fields 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /languages/instant-articles.pot: -------------------------------------------------------------------------------- 1 | #, fuzzy 2 | msgid "" 3 | msgstr "" 4 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 5 | "Project-Id-Version: Instant Articles for WP\n" 6 | "POT-Creation-Date: 2015-11-27 11:17+0100\n" 7 | "PO-Revision-Date: 2015-11-12 20:54+0100\n" 8 | "Last-Translator: Bjørn Johansen \n" 9 | "Language-Team: Bjørn Johansen \n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "X-Generator: Poedit 1.8.6\n" 14 | "X-Poedit-Basepath: ..\n" 15 | "X-Poedit-WPHeader: instant-articles.php\n" 16 | "X-Poedit-SourceCharset: UTF-8\n" 17 | "X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;" 18 | "esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c;" 19 | "_n_noop:1,2;_nx_noop:3c,1,2;__ngettext_noop:1,2\n" 20 | "X-Poedit-SearchPath-0: .\n" 21 | "X-Poedit-SearchPathExcluded-0: *.js\n" 22 | 23 | #: feed-template.php:7 24 | msgid "Instant Articles" 25 | msgstr "" 26 | 27 | #. Plugin Name of the plugin/theme 28 | msgid "Instant Articles for WP" 29 | msgstr "" 30 | 31 | #. Description of the plugin/theme 32 | msgid "Add support for Instant Articles for Facebook to your WordPress site." 33 | msgstr "" 34 | 35 | #. Author of the plugin/theme 36 | msgid "Dekode" 37 | msgstr "" 38 | 39 | #. Author URI of the plugin/theme 40 | msgid "https://dekode.no" 41 | msgstr "" 42 | -------------------------------------------------------------------------------- /languages/de_DE.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 4 | "Project-Id-Version: Instant Articles for WP\n" 5 | "POT-Creation-Date: 2016-05-22 10:10+0200\n" 6 | "PO-Revision-Date: 2016-05-22 10:12+0200\n" 7 | "Language-Team: Bjørn Johansen \n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "X-Generator: Poedit 1.8.7\n" 12 | "X-Poedit-Basepath: ..\n" 13 | "X-Poedit-WPHeader: instant-articles.php\n" 14 | "X-Poedit-SourceCharset: UTF-8\n" 15 | "X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c;_n_noop:1,2;_nx_noop:3c,1,2;__ngettext_noop:1,2\n" 16 | "Last-Translator: \n" 17 | "Language: de\n" 18 | "X-Poedit-SearchPath-0: .\n" 19 | "X-Poedit-SearchPathExcluded-0: *.js\n" 20 | 21 | #: feed-template.php:7 22 | msgid "Instant Articles" 23 | msgstr "Instant Articles" 24 | 25 | #. Plugin Name of the plugin/theme 26 | msgid "Instant Articles for WP" 27 | msgstr "Instant Articles für WP" 28 | 29 | #. Description of the plugin/theme 30 | msgid "Add support for Instant Articles for Facebook to your WordPress site." 31 | msgstr "Ermöglicht die Funtion von Instant Articles für deine WordPress Seite" 32 | 33 | #. Author of the plugin/theme 34 | msgid "Dekode" 35 | msgstr "Dekode" 36 | 37 | #. Author URI of the plugin/theme 38 | msgid "https://dekode.no" 39 | msgstr "https://dekode.no" 40 | -------------------------------------------------------------------------------- /src/wizard/class-instant-articles-option-fb-app.php: -------------------------------------------------------------------------------- 1 | 'Facebook App', 19 | 'description' => '

Configure your Facebook App to enable auto-invalidation of the cache when updating articles

', 20 | ); 21 | 22 | public static $fields = array( 23 | 'app_id' => array( 24 | 'visible' => true, 25 | 'label' => 'Facebook App ID', 26 | 'default' => '', 27 | 'description' => 'Provide a valid App ID', 28 | ), 29 | 'app_secret' => array( 30 | 'visible' => true, 31 | 'label' => 'Facebook App Secret', 32 | 'default' => '', 33 | 'description' => 'Provide a valid App Secret', 34 | ), 35 | 36 | 'page_access_token' => array( 37 | 'visible' => true, 38 | 'label' => 'Page Access Token', 39 | 'default' => '', 40 | 'description' => 'Provide a valid access token for your Page', 41 | ), 42 | ); 43 | 44 | /** 45 | * Constructor. 46 | * 47 | * @since 0.4 48 | */ 49 | public function __construct() { 50 | parent::__construct( 51 | self::OPTION_KEY, 52 | self::$sections, 53 | self::$fields 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/compat/class-instant-articles-comscore.php: -------------------------------------------------------------------------------- 1 | get_raw_embed_code(); 31 | 32 | $registry[ $identifier ] = [ 33 | 'name' => $display_name, 34 | 'payload' => $embed_code, 35 | ]; 36 | } 37 | 38 | /** 39 | * Returns the Comscore tracking code 40 | * 41 | * @since 0.3 42 | */ 43 | public function get_raw_embed_code() { 44 | $settings_analytics = Instant_Articles_Option_Analytics::get_option_decoded(); 45 | if ( empty( $settings_analytics['comscore_id'] ) ) { 46 | return ''; 47 | } 48 | 49 | $comscore_id = (int) $settings_analytics['comscore_id']; 50 | if ( ! $comscore_id ) { 51 | return ''; 52 | } 53 | 54 | $file_path = __DIR__ . '/comscore.js'; 55 | $js = sprintf( file_get_contents( $file_path ), $comscore_id ); 56 | 57 | $code = ''; 58 | 59 | return $code; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/compat/class-instant-articles-gtm4wp.php: -------------------------------------------------------------------------------- 1 | $display_name, 34 | 'payload' => gtm4wp_wp_header_begin( false ), 35 | ); 36 | } 37 | 38 | public function add_ia_status_to_data_layer( $dataLayer ) { 39 | $dataLayer['instantArticle'] = is_transforming_instant_article(); 40 | 41 | return $dataLayer; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /js/instant-articles-option-ads.js: -------------------------------------------------------------------------------- 1 | jQuery( function () { 2 | var idAdSource = '#' + INSTANT_ARTICLES_OPTION_ADS['option_field_id_source']; 3 | var idFanPlacementId = '#' + INSTANT_ARTICLES_OPTION_ADS['option_field_id_fan']; 4 | var idIframeUrl = '#' + INSTANT_ARTICLES_OPTION_ADS['option_field_id_iframe']; 5 | var idEmbedCode = '#' + INSTANT_ARTICLES_OPTION_ADS['option_field_id_embed']; 6 | var idDimensions = '#' + INSTANT_ARTICLES_OPTION_ADS['option_field_id_dimensions']; 7 | var $rowFanPlacementId = jQuery( idFanPlacementId ).parents( 'tr' ); 8 | var $rowIframeUrl = jQuery( idIframeUrl ) .parents( 'tr' ); 9 | var $rowEmbedCode = jQuery( idEmbedCode ) .parents( 'tr' ); 10 | var $rowDimensions = jQuery( idDimensions ) .parents( 'tr' ); 11 | jQuery( idAdSource ).change( function () { 12 | switch (jQuery( this ).val()) { 13 | case 'fan': 14 | $rowFanPlacementId.show(); 15 | $rowIframeUrl .hide(); 16 | $rowEmbedCode .hide(); 17 | $rowDimensions .show(); 18 | break; 19 | 20 | case 'iframe': 21 | $rowFanPlacementId.hide(); 22 | $rowIframeUrl .show(); 23 | $rowEmbedCode .hide(); 24 | $rowDimensions .show(); 25 | break; 26 | 27 | case 'embed': 28 | $rowFanPlacementId.hide(); 29 | $rowIframeUrl .hide(); 30 | $rowEmbedCode .show(); 31 | $rowDimensions .show(); 32 | break; 33 | 34 | default: 35 | $rowFanPlacementId.hide(); 36 | $rowIframeUrl .hide(); 37 | $rowEmbedCode .hide(); 38 | $rowDimensions .hide(); 39 | break; 40 | } 41 | } ).trigger( 'change' ); 42 | } ); 43 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale monitor 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | # Allow manually triggering the workflow. 7 | workflow_dispatch: 8 | 9 | jobs: 10 | stale: 11 | name: Stale 12 | runs-on: ubuntu-latest 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | steps: 17 | - name: Process stale issues and pull requests 18 | uses: actions/stale@main 19 | id: stale 20 | with: 21 | close-issue-label: "[Status] Autoclosed" 22 | close-pr-label: "[Status] Autoclosed" 23 | days-before-close: "7" 24 | days-before-stale: "30" 25 | ascending: true 26 | operations-per-run: 200 27 | exempt-all-milestones: true 28 | stale-issue-label: "[Status] Stale" 29 | stale-issue-message: > 30 | This issue has been marked stale because it has been open for 31 | 30 days with no activity. If there is no activity within 7 days, 32 | it will be closed. 33 | This is an automation to keep issues manageable and actionable and is 34 | not a comment on the quality of this issue nor on the work done so 35 | far. Closed issues are still valuable to the project and are 36 | available to be searched. 37 | stale-pr-label: "[Status] Stale" 38 | stale-pr-message: > 39 | This pull request has been marked stale because it has been open for 40 | 30 days with no activity. If there is no activity within 7 days, 41 | it will be closed. 42 | This is an automation to keep pull requests manageable and actionable 43 | and is not a comment on the quality of this pull request nor on the 44 | work done so far. Closed PRs are still valuable to the project and 45 | their branches are preserved. 46 | -------------------------------------------------------------------------------- /src/compat/class-instant-articles-co-authors-plus.php: -------------------------------------------------------------------------------- 1 | ID = $coauthor->ID; 39 | $author->display_name = is_a( $coauthor, 'WP_User' ) ? $coauthor->data->display_name : $coauthor->display_name; 40 | $author->first_name = $coauthor->first_name; 41 | $author->last_name = $coauthor->last_name; 42 | $author->user_login = is_a( $coauthor, 'WP_User' ) ? $coauthor->data->user_login : $coauthor->user_login; 43 | $author->user_nicename = is_a( $coauthor, 'WP_User' ) ? $coauthor->data->user_nicename : $coauthor->user_nicename; 44 | $author->user_email = is_a( $coauthor, 'WP_User' ) ? $coauthor->data->user_email : $coauthor->user_email; 45 | $author->user_url = is_a( $coauthor, 'WP_User' ) ? $coauthor->data->user_url : $coauthor->website; 46 | $author->bio = $coauthor->description; 47 | 48 | $authors[] = $author; 49 | } 50 | } 51 | 52 | return $authors; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/compat/class-instant-articles-google-analytics-for-wordpress.php: -------------------------------------------------------------------------------- 1 | get_raw_embed_code(); 38 | 39 | $registry[ $identifier ] = array( 40 | 'name' => $display_name, 41 | 'payload' => $embed_code, 42 | ); 43 | } 44 | 45 | /** 46 | * Returns the GA tracking code 47 | * 48 | * @since 0.3 49 | */ 50 | public function get_raw_embed_code() { 51 | 52 | ob_start(); 53 | 54 | if ( function_exists( 'monsterinsights_tracking_script' ) ) { 55 | monsterinsights_tracking_script(); 56 | } else { 57 | $options = Yoast_GA_Options::instance()->options; 58 | 59 | if ( isset( $options['enable_universal'] ) && 1 === (int) $options['enable_universal'] ) { 60 | $tracker = new Yoast_GA_Universal(); 61 | } else { 62 | $tracker = new Yoast_GA_JS(); 63 | } 64 | 65 | $tracker->tracking(); 66 | } 67 | 68 | $ga_code = ob_get_clean(); 69 | 70 | return $ga_code; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /wpcom-helper.php: -------------------------------------------------------------------------------- 1 | instant_article; 29 | 30 | // Create the wpcom stats code. 31 | $hostname = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // input var okay 32 | 33 | $url = 'https://pixel.wp.com/b.gif?host=' . $hostname . '&blog=' . $current_blog->blog_id . '&post=' . $ia_post->get_the_id() . '&subd=' . str_replace( '.wordpress.com', '', $current_blog->domain ) . '&ref=&feed=1'; 34 | 35 | $pixel_html = ''; 38 | 39 | // Create our FBIA markup 40 | $fbia_markup = Analytics::create(); 41 | $fbia_markup->withHTML( $pixel_html ); 42 | 43 | // Add the FBIA-compatible stats markup to the IA content 44 | $instant_article->addChild( $fbia_markup ); 45 | 46 | } 47 | add_action( 'instant_articles_after_transform_post', 'wpcom_fbia_add_stats_pixel' ); 48 | 49 | // make sure these function run in wp.com environment where `plugins_loaded` is already fired when loading the plugin 50 | add_action( 'after_setup_theme', 'instant_articles_load_textdomain' ); 51 | add_action( 'after_setup_theme', 'instant_articles_load_compat' ); 52 | -------------------------------------------------------------------------------- /src/compat/class-instant-articles-yoast-seo.php: -------------------------------------------------------------------------------- 1 | user_url === '' ) { 55 | $facebook_profile_url = get_user_meta( $author->ID, 'facebook', true ); 56 | if ( $facebook_profile_url !== '' ) { 57 | $author->user_url = $facebook_profile_url; 58 | $author->user_url_rel = 'facebook'; 59 | } 60 | } 61 | } 62 | 63 | return $authors; 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | array( $plugin_root_file_path ), 36 | ); 37 | 38 | require_once dirname( __DIR__ ) . '/vendor/yoast/wp-test-utils/src/WPIntegration/bootstrap-functions.php'; 39 | 40 | /* 41 | * Load WordPress, which will load the Composer autoload file, and load the 42 | * MockObject autoloader after that. 43 | */ 44 | WPIntegration\bootstrap_it(); 45 | 46 | if ( ! defined( 'WP_PLUGIN_DIR' ) || file_exists( WP_PLUGIN_DIR . '/' . $plugin_root_file_path ) === false ) { 47 | echo PHP_EOL, 'ERROR: Please check whether the WP_PLUGIN_DIR environment variable is set and set to the correct value. The unit test suite won\'t be able to run without it.', PHP_EOL; 48 | exit( 1 ); 49 | } 50 | 51 | // Additional necessary requires, such as custom TestCases. 52 | } 53 | -------------------------------------------------------------------------------- /src/wizard/class-instant-articles-option-styles.php: -------------------------------------------------------------------------------- 1 | 'Appearance', 19 | ); 20 | 21 | public static $fields = array( 22 | 23 | 'article_style' => array( 24 | 'label' => 'Article Style', 25 | 'default' => 'default', 26 | 'description' => '

Assign your Instant Articles a custom style. To begin, customize a template using the Style Editor. Next, input the name of the style below.

Note: If this field is left blank, the plugin will enable the “Default” style. Learn more about Instant Articles style options in the Design Guide.

', 27 | ), 28 | 29 | 'copyright' => array( 30 | 'label' => 'Copyright', 31 | 'default' => '', 32 | 'description' => 'The copyright details for your articles. Note: Some inline html tags can be used in this field. Learn more about Footer in Instant Articles.', 33 | ), 34 | 35 | 'rtl_enabled' => array( 36 | 'label' => 'Right-to-Left Publishing', 37 | 'render' => 'checkbox', 38 | 'default' => false, 39 | 'description' => 'Body text will read right to left for all articles.', 40 | 'checkbox_label' => 'Enable Right-to-Left (RTL) publishing', 41 | ), 42 | 43 | ); 44 | 45 | /** 46 | * Constructor. 47 | * 48 | * @since 0.4 49 | */ 50 | public function __construct() { 51 | parent::__construct( 52 | self::OPTION_KEY, 53 | self::$sections, 54 | self::$fields 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/InstantArticlesPostTest.php: -------------------------------------------------------------------------------- 1 | user->create(); 24 | $post = self::factory()->post->create_and_get( 25 | array( 26 | 'post_author' => $user_id, 27 | 'post_title' => 'Article title', 28 | 'post_content' => 'something', 29 | 'post_excerpt' => 'This is the excerpt.', 30 | 'post_date' => '', 31 | 'post_modified' => '', 32 | ) 33 | ); 34 | $this->post_id = $post->ID; 35 | $this->instant_articles_post = new Instant_Articles_Post( $post ); 36 | } 37 | 38 | public function test_can_create_instance(): void { 39 | self::assertInstanceOf( 'Instant_Articles_Post', $this->instant_articles_post ); 40 | } 41 | 42 | public function test_can_get_post_fields(): void { 43 | self::assertSame( 'Article title', $this->instant_articles_post->get_the_title() ); 44 | self::assertSame( 'Article title', $this->instant_articles_post->get_the_title_rss() ); 45 | self::assertSame( 'http://' . WP_TESTS_DOMAIN . '/?p=' . $this->post_id, $this->instant_articles_post->get_canonical_url() ); 46 | self::assertIsString( $this->instant_articles_post->get_the_excerpt(), 'Expected string assertion failed.' ); 47 | self::assertIsString( $this->instant_articles_post->get_the_excerpt_rss(), 'Expected string assertion failed.' ); 48 | } 49 | 50 | public function test_featured_image_is_array(): void { 51 | self::assertIsArray( $this->instant_articles_post->get_the_featured_image() ); 52 | } 53 | 54 | public function test_kicker_is_empty_for_no_category(): void { 55 | self::assertEmpty( $this->instant_articles_post->get_the_kicker() ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/compat/jetpack-rules-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "class": "IgnoreRule", 5 | "selector": "p.jetpack-slideshow-noscript" 6 | }, 7 | { 8 | "class": "CaptionRule", 9 | "selector": "div.wp-caption-text" 10 | }, 11 | { 12 | "class": "PassThroughRule", 13 | "selector": "div.gallery-row" 14 | }, 15 | { 16 | "class": "PassThroughRule", 17 | "selector": "div.tiled-gallery p" 18 | }, 19 | { 20 | "class": "PassThroughRule", 21 | "selector": "div.gallery-row p" 22 | }, 23 | { 24 | "class": "PassThroughRule", 25 | "selector": "div.gallery-group p" 26 | }, 27 | { 28 | "class": "PassThroughRule", 29 | "selector": "div.gallery-group" 30 | }, 31 | { 32 | "class": "ImageRule", 33 | "selector": "div.wp-caption", 34 | "properties": { 35 | "image.url": { 36 | "type": "string", 37 | "selector": "img", 38 | "attribute": "src" 39 | } 40 | } 41 | }, 42 | { 43 | "class": "SlideshowImageRule", 44 | "selector": "div.tiled-gallery-item", 45 | "properties": { 46 | "image.url": { 47 | "type": "string", 48 | "selector": "img", 49 | "attribute": "data-orig-file" 50 | }, 51 | "caption.title": { 52 | "type": "string", 53 | "selector": "div.tiled-gallery-caption" 54 | } 55 | } 56 | }, 57 | { 58 | "class": "SlideshowRule", 59 | "selector": "div.tiled-gallery" 60 | }, 61 | { 62 | "class": "SlideshowImageRule", 63 | "selector": "dl.gallery-item", 64 | "properties": { 65 | "image.url": { 66 | "type": "string", 67 | "selector": "a", 68 | "attribute": "href" 69 | }, 70 | "caption.title": { 71 | "type": "string", 72 | "selector": "dd.wp-caption-text" 73 | } 74 | } 75 | }, 76 | { 77 | "class": "Compat\\JetpackSlideshowRule", 78 | "selector": "div.jetpack-slideshow", 79 | "properties": { 80 | "jetpack.data-gallery": { 81 | "type": "json", 82 | "selector": "div.jetpack-slideshow", 83 | "attribute": "data-gallery" 84 | } 85 | } 86 | }, 87 | { 88 | "class": "CaptionRule", 89 | "selector": "div.tiled-gallery-caption" 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /src/wizard/templates/advanced-template.php: -------------------------------------------------------------------------------- 1 | 11 | 12 |

Facebook Instant Articles Settings

13 |
14 | 15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 |
style="display: none;"> 25 |
26 | 27 |
28 |

Configure settings for your styles, ads, analytics and publishing in Instant Articles. Review our developer documentation to learn more.

29 |
30 | 31 |
32 | 33 |
34 | 35 |
36 | 37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /css/instant-articles-meta-box.css: -------------------------------------------------------------------------------- 1 | .instant-articles-dev-mode-indicator { 2 | float: right; 3 | text-decoration: none; 4 | font-size: smaller; 5 | } 6 | 7 | .instant-articles-messages { 8 | margin-left: 15px; 9 | margin-top: 0px; 10 | } 11 | 12 | .instant-articles-messages li { 13 | display: table-row; 14 | } 15 | 16 | .instant-articles-messages li > div, 17 | .instant-articles-messages li > span { 18 | display: table-cell; 19 | } 20 | 21 | .instant-articles-messages li > div { 22 | padding-left: 5px; 23 | } 24 | 25 | .instant-articles-transformer-markup { 26 | display: none; 27 | } 28 | 29 | .instant-articles-show-debug .instant-articles-transformer-markup { 30 | display: block; 31 | } 32 | 33 | .instant-articles-messages li .message span { 34 | display: none; 35 | } 36 | 37 | .instant-articles-messages li:hover .message span { 38 | display: block; 39 | position: absolute; 40 | width: 50%; 41 | height: auto; 42 | border: 1px solid #bb9; 43 | padding: 5px; 44 | background: #ffe; 45 | overflow-y: auto; 46 | color: #666; 47 | margin: 10px; 48 | } 49 | .instant-articles-transformer-markup { 50 | border-top: 1px dashed #ccc; 51 | padding-top: 10px; 52 | margin-top: 10px; 53 | } 54 | .instant-articles-transformer-markup div { 55 | width: 50%; 56 | float: left; 57 | box-sizing: padding-box; 58 | } 59 | 60 | .instant-articles-transformer-markup div textarea { 61 | width: 100%; 62 | height: 400px; 63 | } 64 | 65 | /* Spinner code */ 66 | @keyframes instant_articles_spinner { 67 | to {transform: rotate(360deg);} 68 | } 69 | 70 | @-webkit-keyframes instant_articles_spinner { 71 | to {-webkit-transform: rotate(360deg);} 72 | } 73 | 74 | .instant_articles_spinner { 75 | min-width: 24px; 76 | min-height: 24px; 77 | margin: 20px; 78 | } 79 | 80 | .instant_articles_spinner:before { 81 | content: 'Loading…'; 82 | position: absolute; 83 | top: 50%; 84 | left: 50%; 85 | width: 16px; 86 | height: 16px; 87 | margin-top: -10px; 88 | margin-left: -10px; 89 | } 90 | 91 | .instant_articles_spinner:not(:required):before { 92 | content: ''; 93 | border-radius: 50%; 94 | border-top: 2px solid #03ade0; 95 | border-right: 2px solid transparent; 96 | animation: instant_articles_spinner .6s linear infinite; 97 | -webkit-animation: instant_articles_spinner .6s linear infinite; 98 | } 99 | -------------------------------------------------------------------------------- /src/compat.php: -------------------------------------------------------------------------------- 1 | init(); 15 | } 16 | 17 | // Load compat layer for Yoast SEO. 18 | if ( defined( 'WPSEO_VERSION' ) && ! defined( 'WPSEO_IA_COMPAT' ) ) { 19 | require __DIR__ . '/compat/class-instant-articles-yoast-seo.php'; 20 | $yseo = new Instant_Articles_Yoast_SEO(); 21 | $yseo->init(); 22 | } 23 | 24 | // Load support for Google Analytics for WordPress by MonsterInsights. 25 | if ( ( defined( 'GAWP_VERSION' ) || function_exists( 'MonsterInsights' ) ) && ! defined( 'GAWP_IA_COMPAT' ) ) { 26 | require __DIR__ . '/compat/class-instant-articles-google-analytics-for-wordpress.php'; 27 | $gawp = new Instant_Articles_Google_Analytics_For_WordPress(); 28 | $gawp->init(); 29 | } 30 | 31 | // Load support for Google Tag Manager for WordPress by Duracelltomi. 32 | if ( defined( 'GTM4WP_VERSION' ) ) { 33 | require __DIR__ . '/compat/class-instant-articles-gtm4wp.php'; 34 | $gtm4wp = new Instant_Articles_Google_Tag_Manager_For_WordPress(); 35 | $gtm4wp->init(); 36 | } 37 | 38 | // Load support for Jetpack 39 | if ( defined( 'JETPACK__VERSION' ) ) { 40 | require __DIR__ . '/compat/class-instant-articles-jetpack.php'; 41 | $jp = new Instant_Articles_Jetpack(); 42 | $jp->init(); 43 | } 44 | 45 | // Load support for Get The Image plugin 46 | if ( function_exists( 'get_the_image' ) ) { 47 | require __DIR__ . '/compat/class-instant-articles-get-the-image.php'; 48 | $gti = new Instant_Articles_Get_The_Image(); 49 | $gti->init(); 50 | } 51 | 52 | // Load support for Playbuzz plugin by default #515 53 | require __DIR__ . '/compat/class-instant-articles-playbuzz.php'; 54 | $playbuzz = new Instant_Articles_Playbuzz(); 55 | $playbuzz->init(); 56 | 57 | // Load support for Apester's plugin Medias 58 | require __DIR__ . '/compat/class-instant-articles-apester.php'; 59 | $apester = new Instant_Articles_Apester(); 60 | $apester->init(); 61 | 62 | // Load support for Comscore 63 | require __DIR__ . '/compat/class-instant-articles-comscore.php'; 64 | $comscore = new Instant_Articles_Comscore(); 65 | $comscore->init(); 66 | -------------------------------------------------------------------------------- /src/class-instant-articles-signer.php: -------------------------------------------------------------------------------- 1 | request === self::PUBLIC_KEY_PATH ) { 28 | status_header(200); 29 | echo self::get_public_key(); 30 | die(); 31 | } 32 | } 33 | 34 | public static function get_public_key() { 35 | $public_key = get_option( self::PUBLIC_KEY_OPTION ); 36 | if ( ! $public_key ) { 37 | self::gen_keys(); 38 | $public_key = get_option( self::PUBLIC_KEY_OPTION ); 39 | } 40 | return $public_key; 41 | } 42 | 43 | public static function get_private_key() { 44 | $private_key = get_option( self::PRIVATE_KEY_OPTION ); 45 | if ( ! $private_key ) { 46 | self::gen_keys(); 47 | $private_key = get_option( self::PRIVATE_KEY_OPTION ); 48 | } 49 | return $private_key; 50 | } 51 | 52 | private static function gen_keys( $force = false ) { 53 | $public_key = get_option( self::PUBLIC_KEY_OPTION ); 54 | $private_key = get_option( self::PRIVATE_KEY_OPTION ); 55 | 56 | if ( !$force && $private_key && $public_key ) { 57 | return; 58 | } 59 | 60 | // Create the private and public key 61 | $result = openssl_pkey_new(); 62 | 63 | // Extract the private key from $result to $private_key 64 | openssl_pkey_export( $result, $private_key ); 65 | 66 | // Extract the public key from $result to $public_key 67 | $public_key = openssl_pkey_get_details( $result ); 68 | $public_key = $public_key['key']; 69 | 70 | update_option( self::PRIVATE_KEY_OPTION, $private_key ); 71 | update_option( self::PUBLIC_KEY_OPTION, $public_key ); 72 | } 73 | 74 | public static function get_signature( $data ) { 75 | openssl_sign( $data, $signature, self::get_private_key(), OPENSSL_ALGO_SHA1 ); 76 | return urlencode( base64_encode( $signature ) ); 77 | } 78 | 79 | public static function sign_request_path( $path ) { 80 | $now = new DateTime(); 81 | $ts = $now->getTimestamp(); 82 | $path = add_query_arg( 'ts', $ts, $path ); 83 | $signature = self::get_signature( urldecode($path) ); 84 | $path = add_query_arg( 'hmac', $signature, $path ); 85 | return $path; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /feed-template.php: -------------------------------------------------------------------------------- 1 | '; 12 | 13 | $last_modified = null; 14 | ?> 15 | 16 | 17 | <?php bloginfo_rss( 'name' ); ?> - <?php esc_html_e( 'Instant Articles', 'instant-articles' ); ?> 18 | 19 | 20 | 21 | get_the_content() ) ) ) { 31 | continue; 32 | } 33 | 34 | // Posts are sorted by modification time, so our first accepted post should be the one last modified. 35 | if ( is_null( $last_modified ) ) { 36 | $last_modified = $instant_article_post->get_the_moddate_iso(); 37 | } 38 | ?> 39 | 40 | <?php echo esc_html( $instant_article_post->get_the_title() ); ?> 41 | get_canonical_url() ); ?> 42 | 43 | to_instant_article()->render(); ?>]]> 44 | 45 | 46 | get_the_excerpt() ); ?>]]> 47 | get_the_pubdate_iso() ); ?> 48 | get_the_moddate_iso() ); ?> 49 | get_the_authors(); ?> 50 | 51 | 52 | display_name ); ?> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | # Run on all pushes and on all pull requests. 5 | # Prevent the "push" build from running when there are only irrelevant changes. 6 | push: 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | 11 | # Cancels all previous workflow runs for the same branch that have not yet completed. 12 | concurrency: 13 | # The concurrency group contains the workflow name and the branch name. 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | test: 19 | name: PHP ${{ matrix.php }} 20 | runs-on: ubuntu-20.04 21 | 22 | env: 23 | WP_VERSION: ${{ matrix.wp }} 24 | 25 | strategy: 26 | # PHP 7.1 uses PHPUnit 7.5.20 27 | # PHP 7.2 uses PHPUnit 8.5.21 28 | # PHP 7.3 uses PHPUnit 9.5.10 29 | # PHP 7.4 uses PHPUnit 9.5.10 30 | # PHP 8.0 uses PHPUnit 9.5.10 31 | # PHP 8.1 uses PHPUnit 9.5.10 32 | # PHP 8.2 uses PHPUnit 9.5.10 33 | # Keys: 34 | # - experimental: Whether the build is "allowed to fail". 35 | matrix: 36 | php: ['7.2', '7.3', '7.4', '8.0', '8.1'] 37 | wp: ['latest'] 38 | experimental: [false] 39 | include: 40 | - php: '7.1' 41 | wp: '5.8.5' 42 | experimental: false 43 | - php: '8.2' 44 | wp: 'trunk' 45 | experimental: true 46 | fail-fast: false 47 | continue-on-error: ${{ matrix.experimental }} 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v3 51 | 52 | # There's a known PHP 8.2 deprecation that is not fixed in the plugin. 53 | - name: Setup ini config 54 | if: ${{ matrix.php >= 8.2 }} 55 | run: echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED' >> $GITHUB_OUTPUT 56 | 57 | - name: Setup PHP ${{ matrix.php }} 58 | uses: shivammathur/setup-php@v2 59 | with: 60 | php-version: ${{ matrix.php }} 61 | extensions: ${{ matrix.extensions }} 62 | ini-values: ${{ matrix.ini-values }} 63 | 64 | - name: Setup problem matchers for PHP 65 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" 66 | 67 | - name: Setup Problem Matchers for PHPUnit 68 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 69 | 70 | - name: Install Composer dependencies 71 | uses: ramsey/composer-install@v2 72 | 73 | - name: Start MySQL Service 74 | run: sudo systemctl start mysql.service 75 | 76 | - name: Setting mysql_native_password for PHP <= 7.3 77 | if: ${{ matrix.php <= 7.3 }} 78 | run: mysql -u root -proot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';" 79 | 80 | - name: Prepare environment for integration tests 81 | run: composer prepare-ci --no-interaction 82 | 83 | - name: Run integration tests (multi site) 84 | run: composer test-ms --no-interaction 85 | -------------------------------------------------------------------------------- /.github/workflows/cs-lint.yml: -------------------------------------------------------------------------------- 1 | name: CS & Lint 2 | 3 | on: 4 | # Run on all pushes and on all pull requests. 5 | # Prevent the "push" build from running when there are only irrelevant changes. 6 | push: 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | # Allow manually triggering the workflow. 11 | workflow_dispatch: 12 | 13 | # Cancels all previous workflow runs for the same branch that have not yet completed. 14 | concurrency: 15 | # The concurrency group contains the workflow name and the branch name. 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | checkcs: 21 | name: 'Basic CS and QA checks' 22 | runs-on: ubuntu-20.04 23 | 24 | env: 25 | XMLLINT_INDENT: ' ' 26 | 27 | steps: 28 | - name: Setup PHP 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: '7.4' 32 | coverage: none 33 | tools: cs2pr 34 | 35 | # Show PHP lint violations inline in the file diff. 36 | # @link https://github.com/marketplace/actions/xmllint-problem-matcher 37 | - name: Register PHP lint violations to appear as file diff comments 38 | uses: korelstar/phplint-problem-matcher@v1 39 | 40 | # Show XML violations inline in the file diff. 41 | # @link https://github.com/marketplace/actions/xmllint-problem-matcher 42 | - name: Register XML violations to appear as file diff comments 43 | uses: korelstar/xmllint-problem-matcher@v1 44 | 45 | - name: Checkout code 46 | uses: actions/checkout@v3 47 | 48 | # Validate the composer.json file. 49 | # @link https://getcomposer.org/doc/03-cli.md#validate 50 | - name: Validate Composer installation 51 | run: composer validate --no-check-all --strict --no-interaction 52 | 53 | # Install dependencies and handle caching in one go. 54 | # @link https://github.com/marketplace/actions/install-composer-dependencies 55 | - name: Install Composer dependencies 56 | uses: ramsey/composer-install@v2 57 | 58 | # Lint PHP. 59 | - name: Lint PHP against parse errors 60 | run: composer lint-ci --no-interaction | cs2pr 61 | 62 | # Needed as runs-on: system doesn't have xml-lint by default. 63 | # @link https://github.com/marketplace/actions/xml-lint 64 | - name: Lint phpunit.xml.dist 65 | uses: ChristophWurst/xmllint-action@v1 66 | with: 67 | xml-file: ./phpunit.xml.dist 68 | xml-schema-file: ./vendor/phpunit/phpunit/phpunit.xsd 69 | 70 | - name: Lint .phpcs.xml.dist 71 | uses: ChristophWurst/xmllint-action@v1 72 | with: 73 | xml-file: ./.phpcs.xml.dist 74 | xml-schema-file: ./vendor/squizlabs/php_codesniffer/phpcs.xsd 75 | 76 | # Check the code-style consistency of the PHP files. 77 | # - name: Check PHP code style 78 | # run: composer cs -- --report-full --report-checkstyle=./phpcs-report.xml 79 | # 80 | # - name: Show PHPCS results in PR 81 | # run: cs2pr ./phpcs-report.xml 82 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/Transformer/WPTransformerTest.php: -------------------------------------------------------------------------------- 1 | loadRules($json_file); 27 | 28 | $html_file = file_get_contents(__DIR__ . '/wp.html'); 29 | 30 | libxml_use_internal_errors(true); 31 | $document = new \DOMDocument(); 32 | $document->loadHTML($html_file); 33 | libxml_use_internal_errors(false); 34 | 35 | $instant_article 36 | ->withCanonicalURL('http://localhost/article') 37 | ->withHeader( 38 | Header::create() 39 | ->withTitle('Peace on earth') 40 | ->addAuthor(Author::create()->withName('bill')) 41 | ->withPublishTime(Time::create(Time::PUBLISHED)->withDatetime( 42 | \DateTime::createFromFormat( 43 | 'j-M-Y G:i:s', 44 | '12-Apr-2016 19:46:51' 45 | ) 46 | )) 47 | ); 48 | 49 | $transformer->transform($instant_article, $document); 50 | $instant_article->addMetaProperty('op:generator:version', '1.0.0'); 51 | $instant_article->addMetaProperty('op:generator:transformer:version', '1.0.0'); 52 | $result = $instant_article->render('', true)."\n"; 53 | $expected = file_get_contents(__DIR__ . '/wp-ia.xml'); 54 | 55 | self::assertSame($expected, $result); 56 | // there must be 3 warnings related to inside
  • that is not supported by IA 57 | self::assertCount(3, $transformer->getWarnings()); 58 | } 59 | 60 | public function test_title_transformed_with_bold(): void { 61 | $transformer = new Transformer(); 62 | $json_file = file_get_contents(__DIR__ . '/wp-rules.json'); 63 | $transformer->loadRules($json_file); 64 | 65 | $title_html_string = '

    Title in bold

    '; 66 | 67 | libxml_use_internal_errors(true); 68 | $document = new \DOMDocument(); 69 | $document->loadHtml($title_html_string); 70 | libxml_use_internal_errors(false); 71 | 72 | $header = Header::create(); 73 | $transformer->transform($header, $document); 74 | 75 | self::assertSame('

    Title in bold

    ', $header->getTitle()->render()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automattic/facebook-instant-articles-wp", 3 | "description": "Add support for Instant Articles for Facebook to your WordPress site.", 4 | "license": "GPL-2.0-or-later", 5 | "type": "wordpress-plugin", 6 | "keywords": [ 7 | "wordpress", 8 | "facebook", 9 | "instant articles", 10 | "amp" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Automattic", 15 | "homepage": "https://automattic.com/" 16 | }, 17 | { 18 | "name": "Dekode", 19 | "homepage": "https://dekode.no/" 20 | }, 21 | { 22 | "name": "Facebook", 23 | "homepage": "https://github.com/facebook" 24 | } 25 | ], 26 | "require": { 27 | "php": ">=7.1", 28 | "facebook/facebook-instant-articles-sdk-extensions-in-php": "dev-php8", 29 | "facebook/facebook-instant-articles-sdk-php": "dev-php8", 30 | "facebook/graph-sdk": "dev-php8", 31 | "symfony/css-selector": "2.8.*" 32 | }, 33 | "require-dev": { 34 | "automattic/vipwpcs": "^2.3", 35 | "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7", 36 | "php-parallel-lint/php-parallel-lint": "^1.0", 37 | "phpcompatibility/php-compatibility": "dev-develop as 9.99.99", 38 | "phpcompatibility/phpcompatibility-wp": "^2.1", 39 | "squizlabs/php_codesniffer": "^3.7", 40 | "wp-coding-standards/wpcs": "^2.3.0", 41 | "yoast/wp-test-utils": "^1" 42 | }, 43 | "repositories": [ 44 | { 45 | "type": "vcs", 46 | "url": "https://github.com/whyisjake/facebook-instant-articles-sdk-php" 47 | }, 48 | { 49 | "type": "vcs", 50 | "url": "https://github.com/whyisjake/facebook-instant-articles-sdk-extensions-in-php" 51 | }, 52 | { 53 | "type": "vcs", 54 | "url": "https://github.com/whyisjake/php-graph-sdk" 55 | } 56 | ], 57 | "minimum-stability": "dev", 58 | "prefer-stable": true, 59 | "config": { 60 | "allow-plugins": { 61 | "dealerdirect/phpcodesniffer-composer-installer": true 62 | } 63 | }, 64 | "scripts": { 65 | "cs": [ 66 | "@php ./vendor/bin/phpcs --severity=1" 67 | ], 68 | "lint": [ 69 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" 70 | ], 71 | "lint-ci": [ 72 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git --checkstyle" 73 | ], 74 | "prepare-ci": [ 75 | "bash bin/install-wp-tests.sh wordpress_test root root localhost" 76 | ], 77 | "test": [ 78 | "@php ./vendor/bin/phpunit --no-coverage --order-by=random -v" 79 | ], 80 | "test-ms": [ 81 | "@putenv WP_MULTISITE=1", 82 | "@composer test" 83 | ] 84 | }, 85 | "scripts-descriptions": { 86 | "cs": "Run PHPCS to checking coding standards for the Facebook Instant Articles plugin.", 87 | "lint": "Run PHP linting on the Facebook Instant Articles plugin.", 88 | "lint-ci": "Run PHP linting on the Facebook Instant Articles plugin with checkstyle output for CI.", 89 | "prepare-ci": "Install the files and setup a database needed to run tests for the Facebook Instant Articles plugin for CI.", 90 | "test": "Run the unit tests for the Facebook Instant Articles plugin.", 91 | "test-ms": "Run the unit tests for the Facebook Instant Articles plugin on a multisite install." 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Custom ruleset for instant-articles plugin. 4 | 5 | 6 | 7 | 8 | 9 | . 10 | 12 | /tests/Facebook/ 13 | /vendor/ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/wizard/class-instant-articles-wizard.php: -------------------------------------------------------------------------------- 1 | ' . esc_html( $link_text ) . ''; 48 | $links[] = $settings_link; 49 | 50 | return $links; 51 | } 52 | 53 | public static function menu_items() { 54 | add_menu_page( 55 | 'Instant Articles Setup Wizard', 56 | 'Instant Articles', 57 | 'manage_options', 58 | 'instant-articles-wizard', 59 | array( 'Instant_Articles_Wizard', 'render' ) 60 | , 'dashicons-facebook' 61 | ); 62 | // Hack to let the URL visible to ajax handlers 63 | update_option( 'instant-articles-wizard-url', menu_page_url( 'instant-articles-wizard', false ) ); 64 | } 65 | 66 | public static function get_url() { 67 | $url = menu_page_url( 'instant-articles-wizard', false ); 68 | 69 | // Needed when calling from ajax 70 | if ( ! $url ) { 71 | $url = get_option( 'instant-articles-wizard-url' ); 72 | } 73 | 74 | return $url; 75 | } 76 | 77 | 78 | public static function get_admin_url() { 79 | return parse_url( admin_url(), PHP_URL_HOST ); 80 | } 81 | 82 | public static function render( $ajax = false ) { 83 | if ( ! current_user_can( 'manage_options' ) ) { 84 | wp_die( esc_html( 'You do not have sufficient permissions to access this page.' ) ); 85 | } 86 | 87 | try { 88 | // Read options (they are used on the templates) 89 | $fb_page_settings = Instant_Articles_Option_FB_Page::get_option_decoded(); 90 | $settings_url = self::get_url(); 91 | 92 | include( __DIR__ . '/templates/advanced-template.php' ); 93 | } catch ( Exception $e ) { 94 | if ( Instant_Articles_Wizard_State::get_current_state() !== Instant_Articles_Wizard_State::STATE_REVIEW_SUBMISSION ) { 95 | // Restarts the wizard 96 | Instant_Articles_Wizard_State::do_transition( Instant_Articles_Wizard_State::STATE_APP_SETUP ); 97 | echo '

    ' . 98 | esc_html( 99 | 'Error processing your request. Check server log for more details. Setup and login again to renew Application credentials. Error message: ' . 100 | $e->getMessage() 101 | ) . '

    '; 102 | self::render( $ajax ); 103 | } 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/wizard/class-instant-articles-option-amp.php: -------------------------------------------------------------------------------- 1 | 'AMP Markup Generation', 21 | 'description' => '

    Settings to control the IA to AMP conversion, check the project\'s repository for more info.

    ', 22 | ); 23 | 24 | public static $fields = array( 25 | 26 | Instant_Articles_AMP_Markup::SETTING_AMP_MARKUP => array( 27 | 'label' => 'Enable Markup (experimental)', 28 | 'description' => 'With this option enabled, posts will also be available in AMP markup. The generated mark-up can be accessed by adding ?amp_markup=1 to the URL of a post.', 29 | 'render' => 'checkbox', 30 | 'default' => false, 31 | 'checkbox_label' => 'Enable AMP markup generation', 32 | ), 33 | 34 | Instant_Articles_AMP_Markup::SETTING_STYLE => array( 35 | 'label' => 'Instant Article JSON Style', 36 | 'description' => 'Please paste the contents of the Style JSON file (downloaded from the Publishing Tools)', 37 | 'default' => '', 38 | 'render' => 'textarea', 39 | ), 40 | 41 | Instant_Articles_AMP_Markup::SETTING_DL_MEDIA => array( 42 | 'label' => 'Automatic Image sizing', 43 | 'description' => 'With this option enabled, images in other servers/domains will be downloaded to get their width and height. Learn more in the official docs', 44 | 'render' => 'checkbox', 45 | 'checkbox_label' => 'Download all external images to get their dimensions (slow)', 46 | 'default' => false, 47 | ), 48 | ); 49 | /** 50 | * Sanitize and return all the field values. 51 | * 52 | * This method receives a payload containing all value for its fields and 53 | * should return the same payload after having been sanitized. 54 | * 55 | * Do not encode the payload as this is performed by the 56 | * universal_sanitize_and_encode_handler() of the parent class. 57 | * 58 | * @param array $field_values array map with key field_id => value. 59 | * @since 4.0 60 | */ 61 | public function sanitize_option_fields( $field_values ) { 62 | $old_settings = Instant_Articles_AMP_Markup::get_settings(); 63 | 64 | if ( isset( $field_values[ Instant_Articles_AMP_Markup::SETTING_STYLE ] ) && !empty($field_values[ Instant_Articles_AMP_Markup::SETTING_STYLE ]) ) { 65 | if ( ! Instant_Articles_AMP_Markup::validate_json( $field_values[ Instant_Articles_AMP_Markup::SETTING_STYLE ] ) ) { 66 | add_settings_error( 67 | Instant_Articles_AMP_Markup::SETTING_STYLE, 68 | 'invalid_json', 69 | 'Invalid Style JSON provided' 70 | ); 71 | 72 | $field_values[ Instant_Articles_AMP_Markup::SETTING_STYLE ] = 73 | $old_settings[ Instant_Articles_AMP_Markup::SETTING_STYLE ] ?? ''; 74 | } 75 | } 76 | 77 | return $field_values; 78 | } 79 | 80 | /** 81 | * Constructor. 82 | * 83 | * @since 4.0 84 | */ 85 | public function __construct() { 86 | parent::__construct( 87 | self::OPTION_KEY, 88 | self::$sections, 89 | self::$fields 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/meta-box/class-instant-articles-meta-box.php: -------------------------------------------------------------------------------- 1 | ID ) ) { 59 | return; 60 | } 61 | 62 | require __DIR__ . '/meta-box-loader-template.php'; 63 | } 64 | 65 | /** 66 | * Renderer for the Metabox. 67 | */ 68 | public static function force_submit() { 69 | $post_id = (int) $_POST['post_ID']; 70 | 71 | if ( ! current_user_can( 'edit_post', $post_id ) ) { 72 | wp_die( -1, 403 ); 73 | } 74 | 75 | check_ajax_referer( 'instant-articles-force-submit-' . $post_id, 'security' ); 76 | $force = sanitize_text_field( $_POST[ 'force' ] ) === 'true'; 77 | update_post_meta( $post_id, IA_PLUGIN_FORCE_SUBMIT_KEY, $force ); 78 | } 79 | 80 | /** 81 | * Renderer for the Metabox. 82 | */ 83 | public static function render_meta_box() { 84 | $post_id = (int) $_POST['post_ID']; 85 | 86 | if ( ! current_user_can( 'edit_post', $post_id ) ) { 87 | wp_die( -1, 403 ); 88 | } 89 | 90 | $ajax_nonce = wp_create_nonce( 'instant-articles-force-submit-' . $post_id ); 91 | $post = get_post( $post_id ); 92 | $adapter = new Instant_Articles_Post( $post ); 93 | 94 | try { 95 | $article = $adapter->to_instant_article(); 96 | $canonical_url = $adapter->get_canonical_url(); 97 | $published = ( 'publish' === $post->post_status ); 98 | $dev_mode = false; 99 | $force_submit = get_post_meta( $post_id, IA_PLUGIN_FORCE_SUBMIT_KEY, true ); 100 | $instant_articles_should_submit_post_filter = apply_filters( 'instant_articles_should_submit_post', true, $adapter ); 101 | 102 | Instant_Articles_Wizard::menu_items(); 103 | $settings_page_href = Instant_Articles_Wizard::get_url(); 104 | 105 | $publishing_settings = Instant_Articles_Option_Publishing::get_option_decoded(); 106 | $publish_with_warnings = $publishing_settings['publish_with_warnings'] ?? false; 107 | $fb_page_settings = Instant_Articles_Option_FB_Page::get_option_decoded(); 108 | $publishing_settings = Instant_Articles_Option_Publishing::get_option_decoded(); 109 | 110 | $dev_mode = isset( $publishing_settings['dev_mode'] ) && $publishing_settings['dev_mode']; 111 | 112 | require __DIR__ . '/meta-box-template.php'; 113 | } catch (Exception $e) { 114 | require __DIR__ . '/meta-box-error.php'; 115 | } 116 | 117 | die(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/meta-box/meta-box-template.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | Development Mode 18 | 19 | 20 | 21 | should_submit_post() ) : ?> 22 |

    23 | 24 | 25 | This post will be available as Instant Article once it is shared on Facebook. 26 | 27 |

    28 |
    29 | 30 |

    31 | 32 | 33 | This post will not be available as Instant Article due to a rule created in your site. 34 | 35 |

    36 |
    37 | 38 |

    39 | 40 | 41 | This post will be available as Instant Article once it is published and shared on Facebook. 42 | 43 |

    44 |
    45 | getHeader() || ! $article->getHeader()->getTitle() ) : ?> 46 |

    47 | 48 | 49 | This post will not be available as Instant Article because it is missing a title. 50 | 51 |

    52 |
    53 | getChildren() ) === 0 ) : ?> 54 |

    55 | 56 | 57 | This post will not be available as Instant Article because it is missing content. 58 | 59 |

    60 |
    61 | 62 |

    63 | 64 | 65 | No Facebook Page was selected. Please configure your page in the 66 | Instant Articles plugin settings. 67 | 68 |

    69 |
    70 | transformer->getWarnings() ) > 0 ) : ?> 71 |

    72 | 73 | 74 | This post will not be available as Instant Article because the transformation raised some warnings. 75 | 76 |

    77 |
    78 | 79 | 80 | 81 | transformer->getWarnings() ) > 0 ) : ?> 82 |

    83 | 84 | This post was transformed into an Instant Article with some warnings 85 | [Learn more | 86 | Transformer rule configuration | 87 | Toggle debug information] 88 |

    89 |
      90 | transformer->getWarnings() as $warning ) : ?> 91 |
    • 92 | 93 |
      94 | 95 | 96 | getNode() ) { 98 | echo esc_html( 99 | $warning->getNode()->ownerDocument->saveHTML( $warning->getNode() ) 100 | ); 101 | } 102 | ?> 103 | 104 |
      105 |
    • 106 | 107 |
    108 | 109 |
    110 |

    111 | /> 112 | Submit this article even with warnings 113 |

    114 | 115 | 116 | 117 |

    118 | 119 | This post was transformed into an Instant Article with no warnings 120 | [Toggle debug information] 121 |

    122 | 123 | 124 |
    125 |
    126 | 127 | 128 |
    129 |
    130 | 131 | 132 |
    133 |
    134 |
    135 | -------------------------------------------------------------------------------- /src/embeds.php: -------------------------------------------------------------------------------- 1 | get_provider( $url ); 44 | 45 | $provider_name = false; 46 | if ( false !== strpos( $provider_url, 'instagram.com' ) ) { 47 | $provider_name = 'instagram'; 48 | } elseif ( false !== strpos( $provider_url, 'twitter.com' ) ) { 49 | $provider_name = 'twitter'; 50 | } elseif ( false !== strpos( $provider_url, 'youtube.com' ) ) { 51 | $provider_name = 'youtube'; 52 | } elseif( false !== strpos( $provider_url, 'vimeo.com' ) ) { 53 | $provider_name = 'vimeo'; 54 | } elseif( false !== strpos( $provider_url, 'vine.co' ) ) { 55 | $provider_name = 'vine'; 56 | } elseif( false !== strpos( $provider_url, 'facebook.com' ) ) { 57 | $provider_name = 'facebook'; 58 | } 59 | 60 | $provider_name = apply_filters( 'instant_articles_social_embed_type', $provider_name, $url ); 61 | 62 | if ( false === $provider_name ) { 63 | // We cannot properly cache `false`, so let's use a different value we can check for. 64 | set_transient( $cache_key, 'no_provider', HOUR_IN_SECONDS * 12 ); 65 | } else { 66 | set_transient( $cache_key, $provider_name ); 67 | } 68 | } 69 | 70 | // Change cacheable `'no_provider'` to `false`. 71 | if ( 'no_provider' === $provider_name ) { 72 | $provider_name = false; 73 | } 74 | 75 | if ( $provider_name ) { 76 | $html = instant_articles_embed_get_html( $provider_name, $html, $url, $attr, $post_id ); 77 | delete_transient( $cache_key ); 78 | } 79 | 80 | return $html; 81 | 82 | } 83 | add_filter( 'embed_oembed_html', 'instant_articles_embed_oembed_html', 10, 4 ); 84 | 85 | 86 | /** 87 | * Filter the embed results for embeds. 88 | * 89 | * @since 0.1 90 | * @param string $provider_name The name of the embed provider. E.g. “instagram” or “youtube”. 91 | * @param string $html The original HTML returned from the external oembed/embed provider. 92 | * @param string $url The URL found in the content. 93 | * @param mixed $attr An array with extra attributes. 94 | * @param int $post_id The post ID. 95 | * @return string The filtered HTML. 96 | */ 97 | function instant_articles_embed_get_html( $provider_name, $html, $url, $attr, $post_id ) { 98 | 99 | // Don't try to fix embeds unless we're in Instant Articles context. 100 | // This prevents mangled output on frontend. 101 | if ( ! is_transforming_instant_article() ) { 102 | return $html; 103 | } 104 | 105 | /** 106 | * Filter the HTML that will go into the Instant Article Social Embed markup. 107 | * 108 | * @since 0.1 109 | * @param string $html The HTML. 110 | * @param string $url The URL found in the content. 111 | * @param mixed $attr An array with extra attributes. 112 | * @param int $post_id The post ID. 113 | */ 114 | $html = apply_filters( "instant_articles_social_embed_{$provider_name}", $html, $url, $attr, $post_id ); 115 | 116 | $html = sprintf( '
    %s
    ', $html ); 117 | 118 | /** 119 | * Filter the Instant Article Social Embed markup. 120 | * 121 | * @since 0.1 122 | * @param string $html The Social Embed markup. 123 | * @param string $url The URL found in the content. 124 | * @param mixed $attr An array with extra attributes. 125 | * @param int $post_id The post ID. 126 | */ 127 | $html = apply_filters( 'instant_articles_social_embed', $html, $url, $attr, $post_id ); 128 | 129 | return $html; 130 | } 131 | -------------------------------------------------------------------------------- /src/wizard/class-instant-articles-option-analytics.php: -------------------------------------------------------------------------------- 1 | 'Analytics', 19 | 'description' => '

    Enable 3rd-party analytics to be used with Instant Articles.

    If you already use a WordPress plugin to manage analytics, you can enable it below. You can also embed code to insert your own trackers and analytics. Learn more about Analytics in Instant Articles.

    ', 20 | ); 21 | 22 | public static $fields = array( 23 | 24 | 'integrations' => array( 25 | 'label' => '3rd party integrations', 26 | 'render' => array( 'Instant_Articles_Option_Analytics', 'custom_render_integrations' ), 27 | 'default' => array(), 28 | ), 29 | 30 | 'comscore_id' => array( 31 | 'label' => 'Comscore ID', 32 | 'render' => 'text', 33 | 'default' => false, 34 | ), 35 | 36 | 'embed_code_enabled' => array( 37 | 'label' => 'Embed code', 38 | 'render' => 'checkbox', 39 | 'default' => false, 40 | 'description' => 'Add code for any other analytics services you wish to use.', 41 | 'checkbox_label' => 'Enable custom embed code', 42 | ), 43 | 44 | 'embed_code' => array( 45 | 'label' => '', 46 | 'render' => 'textarea', 47 | 'placeholder' => '', 48 | 'description' => 'Note: You do not need to include any <op-tracker> tags. The plugin will automatically include them in the article markup.', 49 | 'default' => '', 50 | 'double_encode' => true, 51 | ), 52 | ); 53 | 54 | /** 55 | * Constructor. 56 | * 57 | * @since 0.4 58 | */ 59 | public function __construct() { 60 | parent::__construct( 61 | self::OPTION_KEY, 62 | self::$sections, 63 | self::$fields 64 | ); 65 | wp_localize_script( 'instant-articles-option-analytics', 'INSTANT_ARTICLES_OPTION_ANALYTICS', array( 66 | 'option_field_id_embed_code_enabled' => self::OPTION_KEY . '-embed_code_enabled', 67 | 'option_field_id_embed_code' => self::OPTION_KEY . '-embed_code', 68 | ) ); 69 | } 70 | 71 | /** 72 | * Renders the markup for the `integrations` field. 73 | * 74 | * @param array $args The array with configuration of fields. 75 | * @since 0.4 76 | */ 77 | public static function custom_render_integrations( $args ) { 78 | $name = $args['serialized_with_group'] . '[integrations][]'; 79 | 80 | $compat_plugins = parent::get_registered_compat( 'instant_articles_compat_registry_analytics' ); 81 | 82 | if ( empty( $compat_plugins ) ) { 83 | ?> 84 | 85 | 86 | 87 | $plugin_info ) { 94 | ?> 95 | 104 |
    105 | 108 |

    Select which analytics services you'd like to use with Instant Articles.

    109 | $field_value ) { 126 | $field = self::$fields[ $field_id ]; 127 | 128 | if ( $field_id === 'embed_code' ) { 129 | if ( isset( $field_values['embed_code_enabled'] ) && $field_values['embed_code_enabled'] ) { 130 | $document = new DOMDocument(); 131 | $fragment = $document->createDocumentFragment(); 132 | if ( ! @$fragment->appendXML( $field_value ) ) { 133 | add_settings_error( 134 | 'embed_code', 135 | 'invalid_markup', 136 | 'Invalid HTML markup provided for custom analytics tracker code' 137 | ); 138 | } 139 | } 140 | } 141 | } 142 | 143 | return $field_values; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/compat/class-instant-articles-jetpack.php: -------------------------------------------------------------------------------- 1 | _fix_youtube_embed(); 17 | $this->_fix_facebook_embed(); 18 | add_filter( 'instant_articles_transformer_rules_loaded', array( 'Instant_Articles_Jetpack', 'transformer_loaded' ) ); 19 | } 20 | 21 | /** 22 | * Remove the YouTube embed handling in Jetpack 23 | * 24 | */ 25 | private function _fix_youtube_embed() { 26 | 27 | /** 28 | * Do not "fix" bare URLs on their own line of the form 29 | * http://www.youtube.com/v/9FhMMmqzbD8?fs=1&hl=en_US 30 | * as we have oEmbed to handle those 31 | * Registered in jetpack/modules/shortcodes/youtube.php 32 | */ 33 | wp_embed_unregister_handler( 'wpcom_youtube_embed_crazy_url' ); 34 | } 35 | 36 | /** 37 | * Fix the Facebook embed handling 38 | * 39 | */ 40 | private function _fix_facebook_embed() { 41 | 42 | // Don't try to fix facebook embeds unless we're in Instant Articles context. 43 | // This prevents mangled output on frontend. 44 | if ( ! is_transforming_instant_article() ) { 45 | return; 46 | } 47 | 48 | // All of these are registered in jetpack/modules/shortcodes/facebook.php 49 | 50 | if ( defined( 'JETPACK_FACEBOOK_EMBED_REGEX' ) ) { 51 | wp_embed_unregister_handler( 'facebook' ); 52 | wp_embed_register_handler( 'facebook', JETPACK_FACEBOOK_EMBED_REGEX, array( __CLASS__, 'facebook_embed_handler' ) ); 53 | } 54 | if ( defined( 'JETPACK_FACEBOOK_ALTERNATE_EMBED_REGEX' ) ) { 55 | wp_embed_unregister_handler( 'facebook-alternate' ); 56 | wp_embed_register_handler( 'facebook-alternate', JETPACK_FACEBOOK_ALTERNATE_EMBED_REGEX, array( __CLASS__, 'facebook_embed_handler' ) ); 57 | } 58 | if ( defined( 'JETPACK_FACEBOOK_PHOTO_EMBED_REGEX' ) ) { 59 | wp_embed_unregister_handler( 'facebook-photo' ); 60 | wp_embed_register_handler( 'facebook-photo', JETPACK_FACEBOOK_PHOTO_EMBED_REGEX, array( __CLASS__, 'facebook_embed_handler' ) ); 61 | } 62 | if ( defined( 'JETPACK_FACEBOOK_PHOTO_ALTERNATE_EMBED_REGEX' ) ) { 63 | wp_embed_unregister_handler( 'facebook-alternate-photo' ); 64 | wp_embed_register_handler( 'facebook-alternate-photo', JETPACK_FACEBOOK_PHOTO_ALTERNATE_EMBED_REGEX, array( __CLASS__, 'facebook_embed_handler' ) ); 65 | } 66 | if ( defined( 'JETPACK_FACEBOOK_VIDEO_EMBED_REGEX' ) ) { 67 | wp_embed_unregister_handler( 'facebook-video' ); 68 | wp_embed_register_handler( 'facebook-video', JETPACK_FACEBOOK_VIDEO_EMBED_REGEX, array( __CLASS__, 'facebook_embed_handler' ) ); 69 | } 70 | if ( defined( 'JETPACK_FACEBOOK_VIDEO_ALTERNATE_EMBED_REGEX' ) ) { 71 | wp_embed_unregister_handler( 'facebook-alternate-video' ); 72 | wp_embed_register_handler( 'facebook-alternate-video', JETPACK_FACEBOOK_VIDEO_ALTERNATE_EMBED_REGEX, array( __CLASS__, 'facebook_embed_handler' ) ); 73 | } 74 | } 75 | 76 | public static function facebook_embed_handler( $matches, $attr, $url ) { 77 | 78 | $locale = get_locale(); 79 | 80 | // Source: https://www.facebook.com/translations/FacebookLocales.xml 81 | $fb_locales = array( 'af_ZA', 'ak_GH', 'am_ET', 'ar_AR', 'as_IN', 'ay_BO', 'az_AZ', 'be_BY', 'bg_BG', 'bn_IN', 'br_FR', 'bs_BA', 'ca_ES', 'cb_IQ', 'ck_US', 'co_FR', 'cs_CZ', 'cx_PH', 'cy_GB', 'da_DK', 'de_DE', 'el_GR', 'en_GB', 'en_IN', 'en_PI', 'en_UD', 'en_US', 'eo_EO', 'es_CL', 'es_CO', 'es_ES', 'es_LA', 'es_MX', 'es_VE', 'et_EE', 'eu_ES', 'fa_IR', 'fb_LT', 'ff_NG', 'fi_FI', 'fo_FO', 'fr_CA', 'fr_FR', 'fy_NL', 'ga_IE', 'gl_ES', 'gn_PY', 'gu_IN', 'gx_GR', 'ha_NG', 'he_IL', 'hi_IN', 'hr_HR', 'ht_HT', 'hu_HU', 'hy_AM', 'id_ID', 'ig_NG', 'is_IS', 'it_IT', 'ja_JP', 'ja_KS', 'jv_ID', 'ka_GE', 'kk_KZ', 'km_KH', 'kn_IN', 'ko_KR', 'ku_TR', 'ky_KG', 'la_VA', 'lg_UG', 'li_NL', 'ln_CD', 'lo_LA', 'lt_LT', 'lv_LV', 'mg_MG', 'mi_NZ', 'mk_MK', 'ml_IN', 'mn_MN', 'mr_IN', 'ms_MY', 'mt_MT', 'my_MM', 'nb_NO', 'nd_ZW', 'ne_NP', 'nl_BE', 'nl_NL', 'nn_NO', 'ny_MW', 'or_IN', 'pa_IN', 'pl_PL', 'ps_AF', 'pt_BR', 'pt_PT', 'qc_GT', 'qu_PE', 'rm_CH', 'ro_RO', 'ru_RU', 'rw_RW', 'sa_IN', 'sc_IT', 'se_NO', 'si_LK', 'sk_SK', 'sl_SI', 'sn_ZW', 'so_SO', 'sq_AL', 'sr_RS', 'sv_SE', 'sw_KE', 'sy_SY', 'sz_PL', 'ta_IN', 'te_IN', 'tg_TJ', 'th_TH', 'tk_TM', 'tl_PH', 'tl_ST', 'tr_TR', 'tt_RU', 'tz_MA', 'uk_UA', 'ur_PK', 'uz_UZ', 'vi_VN', 'wo_SN', 'xh_ZA', 'yi_DE', 'yo_NG', 'zh_CN', 'zh_HK', 'zh_TW', 'zu_ZA', 'zz_TR' ); 82 | 83 | // If our locale isn’t supported by Facebook, we’ll fall back to en_US 84 | if ( ! in_array( $locale, $fb_locales, true ) ) { 85 | $locale = 'en_US'; 86 | } 87 | 88 | return '
    '; 89 | } 90 | 91 | public static function transformer_loaded( $transformer ) { 92 | // Appends more rules to transformer 93 | $file_path = __DIR__ . '/jetpack-rules-configuration.json'; 94 | $configuration = file_get_contents( $file_path ); 95 | $transformer->loadRules( $configuration ); 96 | 97 | return $transformer; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/wizard/class-instant-articles-option-publishing.php: -------------------------------------------------------------------------------- 1 | 'Publishing Settings', 19 | ); 20 | 21 | public static $fields = array( 22 | 23 | 'dev_mode' => array( 24 | 'label' => 'Development Mode', 25 | 'description' => 'Articles published while in Development Mode are saved as "drafts" within Facebook and will not be made live. Note: Since articles in "draft" are not reviewed, Development Mode should be disabled when publishing articles to Facebook which you intend to use in your one-time review.', 26 | 'render' => 'checkbox', 27 | 'default' => false, 28 | 'checkbox_label' => 'Enable development mode', 29 | ), 30 | 31 | 'custom_rules_enabled' => array( 32 | 'label' => 'Custom transformer rules', 33 | 'render' => 'checkbox', 34 | 'checkbox_label' => 'Enable custom transformer rules', 35 | 'description' => 'Define your own rules to customize the transformation of your content into Instant Articles', 36 | 'default' => '', 37 | ), 38 | 39 | 'custom_rules' => array( 40 | 'label' => '', 41 | 'render' => 'textarea', 42 | 'placeholder' => '{ "rules": [{ "class": "BoldRule", "selector": "span.bold" }, ... ] }', 43 | 'description' => 'Read more about defining your own custom rules to extend/override the built-in ruleset. If you\'ve defined a common rule which you think this plugin should include by default, tell us about it!', 44 | 'default' => '', 45 | ), 46 | 47 | 'publish_with_warnings' => array( 48 | 'label' => 'Transformation warnings', 49 | 'description' => 'With this option disabled, articles which contain warnings in their transformation process won\'t be available as Instant Articles by default — this can be overridden on individual articles. Note: It is recommended that all transformation warnings be fixed.', 50 | 'render' => 'checkbox', 51 | 'default' => false, 52 | 'checkbox_label' => 'Publish articles containing warnings', 53 | ), 54 | 55 | 'display_warning_column' => array( 56 | 'label' => 'FB IA Status column', 57 | 'description' => 'With this option enabled, a column will be added to post indexes to quickly see whether an article transformation failed, succeeded, or had warnings.', 58 | 'render' => 'checkbox', 59 | 'default' => false, 60 | 'checkbox_label' => 'Enable column "FB IA Status"', 61 | ) 62 | 63 | ); 64 | 65 | /** 66 | * Constructor. 67 | * 68 | * @since 0.4 69 | */ 70 | public function __construct() { 71 | parent::__construct( 72 | self::OPTION_KEY, 73 | self::$sections, 74 | self::$fields 75 | ); 76 | wp_localize_script( 'instant-articles-option-publishing', 'INSTANT_ARTICLES_OPTION_PUBLISHING', array( 77 | 'option_field_id_custom_rules_enabled' => self::OPTION_KEY . '-custom_rules_enabled', 78 | 'option_field_id_custom_rules' => self::OPTION_KEY . '-custom_rules', 79 | ) ); 80 | } 81 | 82 | /** 83 | * Sanitize and return all the field values. 84 | * 85 | * This method receives a payload containing all value for its fields and 86 | * should return the same payload after having been sanitized. 87 | * 88 | * Do not encode the payload as this is performed by the 89 | * universal_sanitize_and_encode_handler() of the parent class. 90 | * 91 | * @param array $field_values array map with key field_id => value. 92 | * @since 0.5 93 | */ 94 | public function sanitize_option_fields( $field_values ) { 95 | foreach ( $field_values as $field_id => $field_value ) { 96 | $field = self::$fields[ $field_id ]; 97 | 98 | switch ( $field_id ) { 99 | case 'dev_mode': 100 | $field_values[ $field_id ] = $field_value 101 | ? '1' 102 | : (string) $field['default']; 103 | break; 104 | 105 | case 'custom_rules': 106 | if ( isset( $field_values['custom_rules_enabled'] ) && $field_values['custom_rules_enabled'] ) { 107 | $custom_rules_json = json_decode( $field_values['custom_rules'] ); 108 | if ( null === $custom_rules_json ) { 109 | $field_values['custom_rules'] = $field['default']; 110 | add_settings_error( 111 | 'custom_embed', 112 | 'invalid_json', 113 | 'Invalid JSON provided for custom rules code' 114 | ); 115 | } 116 | } 117 | break; 118 | 119 | default: 120 | // Should never happen. 121 | break; 122 | } 123 | } 124 | 125 | return $field_values; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/class-instant-articles-amp-markup.php: -------------------------------------------------------------------------------- 1 | should_submit_post() ) { 65 | return; 66 | } 67 | 68 | $url = $adapter->get_canonical_url(); 69 | $url = add_query_arg( self::QUERY_ARG, '1', $url ); 70 | 71 | ?> 72 | 73 | $post->ID, 116 | 'post_type' => 'attachment', 117 | 'numberposts' => 100, 118 | 'post_mime_type' => 'image', 119 | ]; 120 | $image_children = get_children( $query_args ); 121 | 122 | foreach ( $image_children as $img_id => $img ) { 123 | $meta = wp_get_attachment_metadata( $img_id ); 124 | 125 | // Removes the file name from the URL 126 | $url_chunks = explode( '/', $img->guid, -1 ); 127 | $base_image_url = implode( '/', $url_chunks ) . '/'; 128 | 129 | // This is the uploaded original file 130 | $media_sizes[ $img->guid ] = [ $meta['width'], $meta['height'] ]; 131 | 132 | // These are the possible redimensions 133 | foreach ( $meta['sizes'] as $size ) { 134 | $size_url = $base_image_url . $size['file']; 135 | $media_sizes[ $size_url ] = [ $size['width'], $size['height'] ]; 136 | } 137 | } 138 | 139 | $properties[ AMPArticle::MEDIA_SIZES_KEY ] = $media_sizes; 140 | 141 | // Transform the post to an Instant Article. 142 | $adapter = new Instant_Articles_Post( $post ); 143 | $article = $adapter->to_instant_article(); 144 | $article_html = $article->render(); 145 | $amp = AMPArticle::create( $article_html, $properties ); 146 | echo $amp->render(); 147 | 148 | die(); 149 | } 150 | 151 | /** 152 | * Helper function to validate the json string 153 | * 154 | * @param $json_str string JSON string 155 | * 156 | * @return bool true for valid JSON 157 | * @since 4.0 158 | */ 159 | public static function validate_json( $json_str ) { 160 | if ( Type::isTextEmpty( $json_str ) ) { 161 | return false; 162 | } 163 | 164 | json_decode( $json_str ); 165 | 166 | return json_last_error() == JSON_ERROR_NONE; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | if [ -z "${WP_VERSION}" ]; then 13 | WP_VERSION=${5-latest} 14 | fi 15 | SKIP_DB_CREATE=${6-false} 16 | 17 | TMPDIR=${TMPDIR-/tmp} 18 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 19 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 20 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} 21 | 22 | download() { 23 | if [ `which curl` ]; then 24 | curl -s "$1" > "$2"; 25 | elif [ `which wget` ]; then 26 | wget -nv -O "$2" "$1" 27 | fi 28 | } 29 | 30 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 31 | WP_BRANCH=${WP_VERSION%\-*} 32 | WP_TESTS_TAG="branches/$WP_BRANCH" 33 | 34 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 35 | WP_TESTS_TAG="branches/$WP_VERSION" 36 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 37 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 38 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 39 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 40 | else 41 | WP_TESTS_TAG="tags/$WP_VERSION" 42 | fi 43 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 44 | WP_TESTS_TAG="trunk" 45 | else 46 | # http serves a single offer, whereas https serves multiple. we only want one 47 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 48 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 49 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 50 | if [[ -z "$LATEST_VERSION" ]]; then 51 | echo "Latest WordPress version could not be found" 52 | exit 1 53 | fi 54 | WP_TESTS_TAG="tags/$LATEST_VERSION" 55 | fi 56 | set -ex 57 | 58 | install_wp() { 59 | 60 | if [ -d $WP_CORE_DIR ]; then 61 | return; 62 | fi 63 | 64 | mkdir -p $WP_CORE_DIR 65 | 66 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 67 | mkdir -p $TMPDIR/wordpress-nightly 68 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip 69 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ 70 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR 71 | else 72 | if [ $WP_VERSION == 'latest' ]; then 73 | local ARCHIVE_NAME='latest' 74 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 75 | # https serves multiple offers, whereas http serves single. 76 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 77 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 78 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 79 | LATEST_VERSION=${WP_VERSION%??} 80 | else 81 | # otherwise, scan the releases and get the most up to date minor version of the major release 82 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 83 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 84 | fi 85 | if [[ -z "$LATEST_VERSION" ]]; then 86 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 87 | else 88 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 89 | fi 90 | else 91 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 92 | fi 93 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 94 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 95 | fi 96 | 97 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 98 | } 99 | 100 | install_test_suite() { 101 | # portable in-place argument for both GNU sed and Mac OSX sed 102 | if [[ $(uname -s) == 'Darwin' ]]; then 103 | local ioption='-i.bak' 104 | else 105 | local ioption='-i' 106 | fi 107 | 108 | # set up testing suite if it doesn't yet exist 109 | if [ ! -d $WP_TESTS_DIR ]; then 110 | # set up testing suite 111 | mkdir -p $WP_TESTS_DIR 112 | svn co --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 113 | svn co --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 114 | fi 115 | 116 | if [ ! -f wp-tests-config.php ]; then 117 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 118 | # remove all forward slashes in the end 119 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 120 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 123 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 124 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 125 | fi 126 | 127 | } 128 | 129 | install_db() { 130 | 131 | if [ ${SKIP_DB_CREATE} = "true" ]; then 132 | return 0 133 | fi 134 | 135 | # parse DB_HOST for port or socket references 136 | local PARTS=(${DB_HOST//\:/ }) 137 | local DB_HOSTNAME=${PARTS[0]}; 138 | local DB_SOCK_OR_PORT=${PARTS[1]}; 139 | local EXTRA="" 140 | 141 | if ! [ -z $DB_HOSTNAME ] ; then 142 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 143 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 144 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 145 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 146 | elif ! [ -z $DB_HOSTNAME ] ; then 147 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 148 | fi 149 | fi 150 | 151 | # create database 152 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 153 | } 154 | 155 | install_wp 156 | install_test_suite 157 | install_db 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CS & Lint](https://github.com/Automattic/fb-instant-articles/actions/workflows/cs-lint.yml/badge.svg)](https://github.com/Automattic/fb-instant-articles/actions/workflows/cs-lint.yml) [![Integration Tests](https://github.com/Automattic/fb-instant-articles/actions/workflows/tests.yml/badge.svg)](https://github.com/Automattic/fb-instant-articles/actions/workflows/tests.yml) [![Stale monitor](https://github.com/Automattic/fb-instant-articles/actions/workflows/stale.yml/badge.svg)](https://github.com/Automattic/fb-instant-articles/actions/workflows/stale.yml) 2 | # Instant Articles for WP 3 | 4 | Enable [Instant Articles for Facebook](https://developers.facebook.com/docs/instant-articles) on your WordPress site. 5 | 6 | ## Maintenance 🚩 7 | Support for this plugin is provided for PHP 8 migrations, but both Automattic/WPVIP and Facebook/Meta have stopped all active development. At this point in the product lifecycle, publishers should be advised to stop using this plugin, and instead, use direct site traffic for Facebook content. 8 | Meta have announced that Instant Articles will have all support dropped at April 2023. 9 | 10 | ## Description 11 | 12 | This plugin adds support for Instant Articles for Facebook, which is a new way for publishers to distribute fast, interactive stories on Facebook. Instant Articles are preloaded in the Facebook mobile app so they load instantly. 13 | 14 | With the plugin active, a new menu will be available for you to connect to your Facebook Page and start publishing your Instant Articles. You'll also see the status of each Instant Articles submission on the edit page of your posts. 15 | 16 | A best effort is made to generate valid Instant Article markup from your posts' content/metadata and publish it to Facebook. The plugin knows how to transform your posts' markup from a set of rules which forms a mapping between elements in you *source markup* and the valid *Instant Article components*. We refer to this “glue” between the two as the ***Transformer Rules***. 17 | 18 | Built-in to the plugin are many [pre-defined transformer rules](https://github.com/Automattic/facebook-instant-articles-wp/blob/master/rules-configuration.json) which aims to cover standard WordPress installations. If your WordPress content contains elements which are not covered by the built-in ruleset, you can define your own additional rules to extend or override the defaults in the Settings of this plugin, under: **Plugin Configuration** > **Publishing Settings** > **Custom transformer rules**. 19 | 20 | ## Access to Instant Articles 21 | 22 | The current criteria for access to Instant Articles are: 23 | 24 | - your Facebook Page must have an established presence of at least 90 days 25 | - your content adheres to the [Instant Article Policies](https://developers.facebook.com/docs/instant-articles/policy/) 26 | 27 | Before your Instant Articles can be published on Facebook, your feed must undergo an initial review and approval. Facebook requires a minimum number of 10 articles in your feed before being eligible for review. The review process checks that your draft Instant Articles are properly formatted, have content consistency with their mobile web counterparts, and adhere to the [community standards](https://www.facebook.com/communitystandards/) and [content policies](https://www.facebook.com/help/publisher/1348682518563619). 28 | 29 | It's important to note that if you use meta fields to add extra text, images or videos to your Posts, Facebook will expect you to add these to your Instant Articles output too. This plugin includes hooks to help you do that. 30 | 31 | [See Facebook's documentation for full details of the submission process.](https://developers.facebook.com/docs/instant-articles) 32 | 33 | Once your feed has been approved, new posts will automatically be taken live on Instant Articles, and existing posts will be taken live once you update them. In order to stay in the Instant Articles program, you must remain active by creating new content and maintaining a minimal readership. 34 | 35 | ## Installation 36 | 37 | 1. Run `composer install` on the root of the plugin folder. Make sure you have [Composer](https://github.com/composer/composer) installed. 38 | 2. Upload the folder to the `/wp-content/plugins/` directory 39 | 3. Activate the plugin through the 'Plugins' menu in WordPress 40 | 41 | ## Frequently Asked Questions 42 | 43 | **Why is there content from my post missing in the generated Instant Article?** 44 | 45 | More likely than not, this is because there is markup in the body of your post that is not mapped to a recognized Instant Article component. On the “Edit Post” screen for your post, look for additional information about the *transformed* output shown within the **Facebook Instant Articles** module located at the bottom of the screen. 46 | 47 | **Why doesn't my post appear in the list of Instant Articles in the publisher tools?** 48 | Your posts are imported to your library when they are shared on Facebook for the first time. 49 | 50 | Alternatively, you can trigger a manual scrape by pasting your URL on our [Share Debugger](http://developers.facebook.com/tools/debug) 51 | 52 | Only Instant Articles with URLs in [domains you have claimed](https://developers.facebook.com/docs/instant-articles/guides/publishertools#connect) will show up in the Publishing Tools section. 53 | **In the Instant Articles module for my post, what does the “This post was transformed into an Instant Article with some warnings” message mean?** 54 | 55 | When transforming your post into an Instant Article, this plugin will show warnings when it encounters content which might not be valid when published to Facebook. When you see this message, it is recommended to resolve each warning individually. 56 | 57 | **What does the “No rules defined for ____ in the context of ____” warning mean?** 58 | 59 | This plugin transforms your post into an Instant Article by matching markup in your content to one of the [components available](https://github.com/facebook/facebook-instant-articles-sdk-php/blob/master/docs/QuickStart.md#transformer-classes) in Instant Articles markup. Although the plugin contains many [built-in rules](https://github.com/Automattic/facebook-instant-articles-wp/blob/master/rules-configuration.json) to handle common cases, there may be markup in your post which is not recognized by these existing rules. In this case, you may be required to define some of your own rules. See below for more details about where and how. 60 | 61 | **How do I define my own transformer rules so that content from my site is rendered appropriately in an Instant Article?** 62 | 63 | Your custom rules can be defined in the Settings of this plugin, under: **Plugin Configuration** > **Publishing Settings** > **Custom transformer rules**. More detailed instructions about all the options available is documented in the [Custom Transformer Rules](https://github.com/facebook/facebook-instant-articles-sdk-php/blob/master/docs/QuickStart.md#custom-transformer-rules) section of the Facebook Instant Articles SDK. 64 | 65 | **I know of a custom transformer rule which is pretty common in the community. How can it be included by default in the plugin?** 66 | 67 | You can propose popular transformer rules to be included in the plugin by [suggesting it on GitHub](https://github.com/Automattic/facebook-instant-articles-wp/issues/new). 68 | 69 | **How do I post articles to Instant Articles after plugin is installed?** 70 | 71 | In order to import your posts to your Instant Articles library on Facebook you need to [Connect Your Site](https://developers.facebook.com/docs/instant-articles/guides/publishertools#connect) first. Then either share your posts on your page or use the [Sharing Debugger](https://developers.intern.facebook.com/tools/debug/sharing/) to scrape them. After you have 10 articles imported, you will be able to submit them for review. 72 | 73 | **How do I change the feed slug/URL if I'm using the RSS integration?** 74 | 75 | To change the feed slug, set the constant INSTANT_ARTICLES_SLUG to whatever you like. If you do, remember to flush the rewrite rules afterwards. 76 | By default it is set to `instant-articles` which usually will give you a feed URL set to `/feed/instant-articles` 77 | 78 | **How do I flush the rewrite rules after changing the feed slug?** 79 | 80 | Usually simply visiting the permalinks settings page in the WordPress dashboard will do the trick (/wp-admin/options-permalink.php) 81 | 82 | ## Changelog 83 | 84 | Please visit the [changelog](https://github.com/Automattic/fb-instant-articles/blob/develop/CHANGELOG.md). 85 | -------------------------------------------------------------------------------- /src/wizard/class-instant-articles-option-ads.php: -------------------------------------------------------------------------------- 1 | 'Ads', 19 | 'description' => '

    Choose your preferred method for displaying ads in your Instant Articles and input the code in the boxes below. Learn more about your options for advertising in Instant Articles.

    ', 20 | ); 21 | 22 | public static $fields = array( 23 | 24 | 'ad_source' => array( 25 | 'label' => 'Ad Type', 26 | 'description' => 'This plugin will automatically place the ads within your articles.', 27 | 'render' => array( 'Instant_Articles_Option_Ads', 'custom_render_ad_source' ), 28 | 'select_options' => array( 29 | 'none' => 'None', 30 | 'fan' => 'Facebook Audience Network', 31 | 'iframe' => 'Custom iframe URL', 32 | 'embed' => 'Custom Embed Code', 33 | ), 34 | 'default' => 'none', 35 | ), 36 | 37 | 'fan_placement_id' => array( 38 | 'label' => 'Audience Network Placement ID', 39 | 'description' => 'Find your Placement ID for Facebook Audience Network on your app\'s Audience Network Portal', 40 | 'default' => null, 41 | ), 42 | 43 | 'iframe_url' => array( 44 | 'label' => 'Source URL', 45 | 'placeholder' => '//ad-server.com/my-ad', 46 | 'description' => 'Note: Instant Articles only supports Direct Sold ads. No programmatic ad networks, other than Facebook\'s Audience Network, are permitted.', 47 | 'default' => '', 48 | ), 49 | 50 | 'embed_code' => array( 51 | 'label' => 'Embed Code', 52 | 'render' => 'textarea', 53 | 'description' => 'Add code to be used for displayed ads in your Instant Articles.', 54 | 'default' => '', 55 | 'placeholder' => '', 56 | 'double_encode' => true, 57 | ), 58 | 59 | 'dimensions' => array( 60 | 'label' => 'Ad Dimensions', 61 | 'render' => 'select', 62 | 'select_options' => array( 63 | '300x250' => 'Large (300 x 250)', 64 | '320x50' => 'Small (320 x 50)', 65 | ), 66 | 'default' => '300x250', 67 | ), 68 | 69 | ); 70 | 71 | /** 72 | * Constructor. 73 | * 74 | * @since 0.4 75 | */ 76 | public function __construct() { 77 | parent::__construct( 78 | self::OPTION_KEY, 79 | self::$sections, 80 | self::$fields 81 | ); 82 | wp_localize_script( 'instant-articles-option-ads', 'INSTANT_ARTICLES_OPTION_ADS', array( 83 | 'option_field_id_source' => self::OPTION_KEY . '-ad_source', 84 | 'option_field_id_fan' => self::OPTION_KEY . '-fan_placement_id', 85 | 'option_field_id_iframe' => self::OPTION_KEY . '-iframe_url', 86 | 'option_field_id_embed' => self::OPTION_KEY . '-embed_code', 87 | 'option_field_id_dimensions' => self::OPTION_KEY . '-dimensions', 88 | ) ); 89 | } 90 | 91 | /** 92 | * Renders the ad source. 93 | * 94 | * @param array $args configuration fields for the ad. 95 | * @since 0.4 96 | */ 97 | public static function custom_render_ad_source( $args ) { 98 | $id = $args['label_for']; 99 | $name = $args['serialized_with_group'] . '[ad_source]'; 100 | 101 | $description = isset( $args['description'] ) 102 | ? '

    ' . esc_html( $args['description'] ) . '

    ' 103 | : ''; 104 | 105 | ?> 106 | 135 | 136 | $field_value ) { 153 | $field = self::$fields[ $field_id ]; 154 | 155 | switch ( $field_id ) { 156 | case 'ad_source': 157 | $all_options = array(); 158 | 159 | $registered_compat_ads = Instant_Articles_Option::get_registered_compat( 160 | 'instant_articles_compat_registry_ads' 161 | ); 162 | 163 | foreach ( $field['select_options'] as $option_id => $option_info ) { 164 | $all_options[] = $option_id; 165 | } 166 | foreach ( $registered_compat_ads as $compat_id => $compat_info ) { 167 | $all_options[] = $compat_id; 168 | } 169 | 170 | if ( ! in_array( $field_value, $all_options, true ) ) { 171 | $field_values[ $field_id ] = $field['default']; 172 | add_settings_error( 173 | $field_id, 174 | 'invalid_option', 175 | 'Invalid Ad Source' 176 | ); 177 | } 178 | break; 179 | 180 | case 'fan_placement_id': 181 | if ( isset( $field_values['ad_source'] ) && 'fan' === $field_values['ad_source'] ) { 182 | if ( preg_match( '/^[0-9_]+$/', $field_value ) !== 1 ) { 183 | add_settings_error( 184 | $field_id, 185 | 'invalid_placement_id', 186 | 'Invalid Audience Network Placement ID provided' 187 | ); 188 | $field_values[ $field_id ] = $field['default']; 189 | } 190 | } 191 | break; 192 | 193 | case 'iframe_url': 194 | if ( isset( $field_values['ad_source'] ) && 'iframe' === $field_values['ad_source'] ) { 195 | $url = $field_value; 196 | if ( strpos( $url, '//' ) === 0 ) { 197 | // Allow URLs without protocol prefix 198 | $url = 'http:' . $url; 199 | } 200 | $url = filter_var( $url , FILTER_VALIDATE_URL ); 201 | if ( ! $url ) { 202 | $field_values[ $field_id ] = $field['default']; 203 | add_settings_error( 204 | $field_id, 205 | 'invalid_url', 206 | 'Invalid URL provided for Ad iframe' 207 | ); 208 | } 209 | } 210 | break; 211 | 212 | case 'embed_code': 213 | if ( isset( $field_values['ad_source'] ) && 'embed' === $field_values['ad_source'] ) { 214 | $document = new DOMDocument(); 215 | $fragment = $document->createDocumentFragment(); 216 | if ( ! @$fragment->appendXML( $field_value ) ) { 217 | add_settings_error( 218 | 'embed_code', 219 | 'invalid_markup', 220 | 'Invalid HTML markup provided for ad custom embed code' 221 | ); 222 | } 223 | } 224 | break; 225 | 226 | case 'dimensions': 227 | if ( isset( $field_values['ad_source'] ) && 'none' !== $field_values['ad_source'] ) { 228 | if ( ! array_key_exists( $field_value, $field['select_options'] ) ) { 229 | $field_values[ $field_id ] = $field['default']; 230 | add_settings_error( 231 | 'embed_code', 232 | 'invalid_dimensions', 233 | 'Invalid dimensions provided for Ad' 234 | ); 235 | } 236 | } 237 | break; 238 | } 239 | } 240 | 241 | return $field_values; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /css/instant-articles-wizard.css: -------------------------------------------------------------------------------- 1 | #instant_articles_wizard a { 2 | color: #4080ff; 3 | text-decoration: none; 4 | font-weight: bold; 5 | } 6 | #instant_articles_wizard a:hover { 7 | text-decoration: underline; 8 | } 9 | 10 | .instant-articles-card { 11 | background: white; 12 | border: solid 1px #dddfe2; 13 | border-radius: 5px; 14 | margin: 20px 20px 0 0; 15 | overflow: hidden; 16 | } 17 | .instant-articles-card-success { 18 | border: solid 1px #42b72a; 19 | } 20 | .instant-articles-card-fail { 21 | border: solid 1px #eb4145; 22 | } 23 | .instant-articles-card-collapsed { 24 | border: solid 1px #42b72a; 25 | padding-bottom: 0; 26 | } 27 | .instant-articles-card-collapsed .instant-articles-card-content { 28 | display: none; 29 | } 30 | .instant-articles-card-content-box h3 { 31 | text-align: center; 32 | margin-top: 40px; 33 | margin-bottom: 40px; 34 | } 35 | .instant-articles-card-content-box h4 { 36 | text-align: center; 37 | margin-top: 15px; 38 | margin-bottom: -5px; 39 | color: #9ca0a7; 40 | } 41 | .instant-articles-card-steps { 42 | margin: 20px auto; 43 | overflow: hidden; 44 | width: 660px; 45 | } 46 | .instant-articles-card-step { 47 | position: relative; 48 | float: left; 49 | display: inline-block; 50 | width: 140px; 51 | margin: 0px 10px; 52 | } 53 | .instant-articles-card-step img { 54 | display: block; 55 | width: 60px; 56 | margin: 0 auto; 57 | } 58 | .instant-articles-card-step p { 59 | text-align: center; 60 | } 61 | .instant-articles-card-bullet-bar { 62 | margin: 80px auto 20px auto; 63 | min-height: 50px; 64 | overflow: hidden; 65 | width: 660px; 66 | } 67 | .instant-articles-card-bullet-step { 68 | position: relative; 69 | float: left; 70 | display: inline-block; 71 | width: 140px; 72 | margin: 0 10px; 73 | } 74 | .instant-articles-card-bullet-step p { 75 | text-align: center; 76 | color: #9ca0a7; 77 | font-size: 0.95em; 78 | margin: 0.7em 0; 79 | display: none; 80 | } 81 | .instant-articles-card-bullet-step h4 { 82 | text-align: center; 83 | color: #9ca0a7; 84 | font-size: 0.95em; 85 | margin-top: 0.7em; 86 | margin-bottom: -0.5em; 87 | } 88 | .instant-articles-card-bullet { 89 | display: block; 90 | width: 15px; 91 | height: 15px; 92 | background-color: #d8d8d8; 93 | border-radius: 10px; 94 | margin: 0 auto; 95 | color: white; 96 | font-size: 0.55em; 97 | font-weight: lighter; 98 | text-align: center; 99 | line-height: 15px; 100 | } 101 | .instant-articles-card-bullet-path { 102 | position: absolute; 103 | top: 0; 104 | margin-top: 7px; 105 | right: -84px; 106 | width: 146px; 107 | height: 2px; 108 | background-color: #d8d8d8; 109 | } 110 | .instant-articles-card-bullet-step-completed p, 111 | .instant-articles-card-bullet-step-completed h4 { 112 | color: #69c456; 113 | } 114 | .instant-articles-card-bullet-step-completed .instant-articles-card-bullet { 115 | background-color: #69c456; 116 | } 117 | .instant-articles-card-bullet-step-completed .instant-articles-card-bullet::before { 118 | content: '✔'; 119 | } 120 | .instant-articles-card-bullet-step-current p, 121 | .instant-articles-card-bullet-step-current h4 { 122 | display: block; 123 | color: #4080ff; 124 | } 125 | .instant-articles-card-bullet-step-current .instant-articles-card-bullet { 126 | background-color: #4080ff; 127 | } 128 | .instant-articles-card-title { 129 | background: #f6f7f9; 130 | border-radius: 5px 5px 0 0; 131 | border-bottom: solid 1px #dddfe2; 132 | height: 55px; 133 | line-height: 55px; 134 | position: relative; 135 | } 136 | .instant-articles-card-collapsed .instant-articles-card-title { 137 | border-bottom: none; 138 | background: #f6faf1; 139 | } 140 | .instant-articles-card-success .instant-articles-card-title { 141 | border-bottom: solid 1px #42b72a; 142 | background: #f6faf1; 143 | } 144 | .instant-articles-card-fail .instant-articles-card-title { 145 | border-bottom: solid 1px #eb4145; 146 | background: #fcebe8; 147 | } 148 | .instant-articles-card-title h3 { 149 | margin: 0 20px 0 20px; 150 | display: inline-block; 151 | vertical-align: middle; 152 | } 153 | .instant-articles-card-title-right { 154 | margin: 0 20px 0 20px; 155 | display: inline-block; 156 | vertical-align: middle; 157 | position: absolute; 158 | right: 0px; 159 | font-size: 1.3em; 160 | color: #acafb6; 161 | } 162 | .instant-articles-card-collapsed .instant-articles-card-title-right > * { 163 | display: inline-block; 164 | vertical-align: middle; 165 | } 166 | .instant-articles-card-title-right > * { 167 | display: none; 168 | } 169 | .instant-articles-card-title-right img { 170 | width: 40px; 171 | height: 40px; 172 | } 173 | .instant-articles-card-title-step { 174 | display: inline-block; 175 | } 176 | .instant-articles-card-title-checkmark { 177 | color: #42b72a; 178 | font-size: 0.9em; 179 | } 180 | .instant-articles-card-collapsed .instant-articles-card-title-step { 181 | display: none; 182 | } 183 | .instant-articles-card-title-label { 184 | font-weight: bold; 185 | font-size: .8em; 186 | color: #4b4f56; 187 | } 188 | .instant-articles-card-title-value { 189 | font-size: .8em; 190 | color: #373b42; 191 | } 192 | .instant-articles-card-title-edit { 193 | background: url('../assets/pencil.png'); 194 | background-size: 13px 13px; 195 | background-repeat: no-repeat; 196 | width: 15px; 197 | height: 14px; 198 | } 199 | .instant-articles-card-content { 200 | margin: 20px; 201 | } 202 | .instant-articles-card-content-box { 203 | width: 50%; 204 | float: left; 205 | box-sizing: border-box; 206 | } 207 | .instant-articles-card-content-box label { 208 | font-weight: bold; 209 | color: #4b4f56; 210 | font-size: 1em; 211 | } 212 | .instant-articles-card-content-box p { 213 | color: #9ca0a7; 214 | font-size: 1em; 215 | margin: 0.7em 0; 216 | } 217 | .instant-articles-card-content-box ol { 218 | margin-left: 0px; 219 | padding-left: 0px; 220 | list-style: decimal inside none; 221 | } 222 | .instant-articles-card-content-box li { 223 | color: #9ca0a7; 224 | font-size: 1em; 225 | margin: 0.7em 0; 226 | } 227 | .instant-articles-page-img { 228 | width: 40px; 229 | height: 40px; 230 | background-color: black; 231 | border: 1px solid #dddfe2; 232 | margin-left: 5px; 233 | } 234 | .instant-articles-wizard-page-selection li { 235 | margin: 0px -20px; 236 | padding: 10px 20px; 237 | cursor: pointer; 238 | } 239 | .instant-articles-radio-selected { 240 | background-color: #f6f7f9; 241 | } 242 | .instant-articles-wizard-page-selection li > label { 243 | font-weight: normal; 244 | color: #9ca0a7; 245 | margin-left: 10px; 246 | } 247 | .instant-articles-wizard-page-selection li > label span { 248 | display: none; 249 | } 250 | .instant-articles-wizard-page-selection li.instant-articles-radio-selected > label > span { 251 | display: inline-block; 252 | margin-left: 30px; 253 | } 254 | .instant-articles-wizard-page-selection li.instant-articles-radio-selected > label > span.page-enabled { 255 | font-weight: bold; 256 | color: #69c456; 257 | } 258 | .instant-articles-wizard-page-selection li > * { 259 | display:inline-block; 260 | vertical-align:middle; 261 | cursor: pointer; 262 | } 263 | .instant-articles-card-content-right { 264 | padding-left: 10px; 265 | } 266 | .instant-articles-card-content-left { 267 | padding-right: 10px; 268 | } 269 | .instant-articles-card-content-full { 270 | float: none; 271 | width: 100%; 272 | } 273 | .instant-articles-card-content-full .instant-articles-card-title-edit { 274 | display: inline-block; 275 | } 276 | .instant-articles-card-content-full hr { 277 | margin: 20px -20px 20px -20px; 278 | } 279 | .instant-articles-label { 280 | display: block; 281 | } 282 | .instant-articles-input-text { 283 | display: block; 284 | border-radius: 4px; 285 | margin-top: 5px; 286 | margin-bottom: 10px; 287 | } 288 | .instant-articles-button { 289 | display: inline-block; 290 | border-radius: 4px; 291 | background-color: #4267b2; 292 | color: #fff !important; 293 | text-decoration: none !important; 294 | padding: 8px 20px; 295 | margin-top: 10px; 296 | border: none; 297 | line-height: 25px; 298 | vertical-align: middle; 299 | cursor: pointer; 300 | } 301 | a.instant-articles-button { 302 | padding: 5px 20px; 303 | } 304 | .instant-articles-button:hover, 305 | .instant-articles-button:active, 306 | .instant-articles-button:focus { 307 | color: #fff; 308 | } 309 | .instant-articles-button label { 310 | color: white; 311 | font-weight: normal; 312 | font-size: 1em; 313 | margin-top: -2px; 314 | display: inline-block; 315 | height: 20px; 316 | float: left; 317 | } 318 | .instant-articles-button-highlight { 319 | background-color: #42b72a; 320 | } 321 | .instant-articles-button-disabled { 322 | opacity: 0.4; 323 | cursor: default !important; 324 | } 325 | .instant-articles-button-disabled * { 326 | cursor: default !important; 327 | } 328 | .instant-articles-button-centered { 329 | margin: 0 auto; 330 | display: block; 331 | } 332 | .instant-articles-button-disabled:focus, 333 | .instant-articles-button-disabled:active { 334 | box-shadow: none; 335 | outline: none; 336 | } 337 | .instant-articles-button-icon-facebook { 338 | background: url('../assets/icon-fb-login.png'); 339 | background-size: 18px 18px; 340 | background-repeat: no-repeat; 341 | display: inline-block; 342 | width: 18px; 343 | height: 18px; 344 | margin: 0 10px 0 -12px; 345 | float: left; 346 | } 347 | .instant-articles-card-title-link { 348 | text-decoration: none; 349 | font-size: .7em; 350 | font-weight: bold; 351 | } 352 | .instant-articles-advanced-settings { 353 | font-weight: bold; 354 | margin-top: 20px; 355 | } 356 | .instant-articles-wizard-toggle a, 357 | .instant-articles-wizard-toggle a:active, 358 | .instant-articles-wizard-toggle a:focus { 359 | text-decoration: none; 360 | outline: none; 361 | box-shadow: none; 362 | } 363 | .instant-articles-wizard-toggle-opened::before { 364 | content: "▼ "; 365 | } 366 | .instant-articles-wizard-toggle-closed::before { 367 | content: "▶︎ "; 368 | } 369 | .instant-articles-advanced-settings[data-state='opened'] .instant-articles-wizard-toggle-closed { 370 | display: none; 371 | } 372 | .instant-articles-advanced-settings[data-state='closed'] .instant-articles-wizard-toggle-opened, 373 | .instant-articles-advanced-settings[data-state='closed'] + .instant-articles-wizard-advanced-settings-box { 374 | display: none; 375 | } 376 | 377 | #instant_articles_wizard_messages div.error, 378 | #instant_articles_wizard_messages div.update, 379 | #instant_articles_wizard_messages .notice, 380 | #instant_articles_wizard div.error, 381 | #instant_articles_wizard div.update, 382 | #instant_articles_wizard .notice { 383 | margin-left: 0; 384 | } 385 | 386 | #instant_articles_wizard.loading { 387 | opacity: 0.5; 388 | position: relative; 389 | } 390 | 391 | #instant_articles_wizard.loading::after { 392 | content: ''; 393 | border: 16px solid rgba(255,255,255,0); /* Light grey */ 394 | border-top: 16px solid #3498db; /* Blue */ 395 | border-radius: 50%; 396 | width: 120px; 397 | height: 120px; 398 | animation: spin 1s linear infinite; 399 | display: block; 400 | position: absolute; 401 | margin-top: -120px; 402 | margin-left: -120px; 403 | left: 50%; 404 | top: 50%; 405 | } 406 | #instant-articles-wizard-signup { 407 | display: none; 408 | } 409 | #instant-articles-wizard-customize-style-next { 410 | display: none; 411 | } 412 | @keyframes spin { 413 | 0% { transform: rotate(0deg); } 414 | 100% { transform: rotate(360deg); } 415 | } 416 | 417 | @media screen and (max-width: 782px) { 418 | .instant-articles-card-content-box { 419 | clear: both; 420 | width: 100%; 421 | padding: 0; 422 | } 423 | .instant-articles-card-content-right { 424 | margin-top: 40px; 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/Transformer/wp-ia.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
    13 |
    14 |

    Peace on <b>earth</b>

    15 | 16 |
    17 | bill 18 |
    19 |
    20 |

    Yes, peace is good for everybody!
    21 | Man kind.

    22 |
    23 | 24 |
    25 |

    Some text after img.

    26 |

    Some text before img

    27 |
    28 | 29 |
    30 |

    Some text before img

    31 |
    32 | 33 |
    34 |

    and some after img.

    35 |
    36 | 37 |
    38 |

    Some text before iframe

    39 |
    40 | 41 |
    42 |
    43 | 44 |
    45 |

    some after iframe.

    46 |

    Some text before iframe

    47 |
    48 | 49 |
    50 |

    and some after iframe.

    51 |
    52 | 53 |
    54 |
    55 | 60 |
    61 |
    62 | 67 |
    68 |
    69 | 74 |
    75 |
    76 | 80 |
    81 |
    82 | 83 |
    84 |
    85 | 89 |
    90 |
    91 | 92 |
    93 |
    94 | 98 |
    99 |
    100 | 118 |
    119 |
      120 |
    • Some text on li before img
    • 121 |
    • Some text on li before imgand after img
    • 122 |
    • Some text after img
    • 123 |
    124 |
    125 |
    126 | 127 |
    128 |
    129 | 130 |
    131 |
    132 | 133 |
    Image 2
    134 |
    135 |
    136 | 137 |
    Image 3
    138 |
    139 |
    140 | 141 |
    142 |
    143 |
    144 |
    145 | 146 |
    Image 1
    147 |
    148 |
    149 | 150 |
    Image 2
    151 |
    152 |
    153 | 154 |
    Image 3
    155 |
    156 |
    157 |
    158 | 174 |
    175 |
    176 | 183 |
    184 |
    185 | 186 |
    blue eyes
    187 |
    188 |
    189 | 192 |
    193 |

    Standard paragraph that shouldn't lie within the interactive block.

    194 |
    195 | 199 |
    200 |
    201 |
    202 | 203 |
    Caption Img 1
    204 |
    205 |
    206 | 207 |
    Alternative text
    208 |
    209 |
    210 | 211 |
    Caption img 3
    212 |
    213 |
    214 |
    215 | 216 |
    caption
    217 |
    218 |
    219 | 223 |
    224 |
    225 | 229 |
    230 |
    231 | 232 |
    233 |
    234 | 238 |
    239 |
    240 | 241 | 242 | -------------------------------------------------------------------------------- /rules-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": 3 | [{ 4 | "class": "TextNodeRule" 5 | }, { 6 | "class": "PassThroughRule", 7 | "selector": "html" 8 | }, { 9 | "class": "PassThroughRule", 10 | "selector": "head" 11 | }, { 12 | "class": "PassThroughRule", 13 | "selector": "body" 14 | }, { 15 | "class": "PassThroughRule", 16 | "selector" : "code" 17 | },{ 18 | "class": "PassThroughRule", 19 | "selector" : "del" 20 | },{ 21 | "class": "PassThroughRule", 22 | "selector" : "mark" 23 | }, { 24 | "class": "PassThroughRule", 25 | "selector" : "span" 26 | }, { 27 | "class": "PassThroughRule", 28 | "selector": "g" 29 | }, { 30 | "class": "ParagraphRule", 31 | "selector": "p" 32 | }, { 33 | "class": "FooterSmallRule", 34 | "selector": "small" 35 | }, { 36 | "class": "LineBreakRule", 37 | "selector": "br" 38 | }, { 39 | "class": "AnchorRule", 40 | "selector": "a", 41 | "properties": { 42 | "anchor.href": { 43 | "type": "string", 44 | "selector": "a", 45 | "attribute": "href" 46 | }, 47 | "anchor.rel": { 48 | "type": "string", 49 | "selector": "a", 50 | "attribute": "rel" 51 | } 52 | } 53 | }, { 54 | "class": "BoldRule", 55 | "selector": "b" 56 | }, { 57 | "class": "BoldRule", 58 | "selector": "strong" 59 | }, { 60 | "class": "ItalicRule", 61 | "selector": "i" 62 | }, { 63 | "class": "ItalicRule", 64 | "selector": "em" 65 | }, { 66 | "class": "BlockquoteRule", 67 | "selector": "blockquote" 68 | }, { 69 | "class": "PassThroughRule", 70 | "selector": "blockquote p" 71 | }, { 72 | "class": "ItalicRule", 73 | "selector": "cite" 74 | }, { 75 | "class": "ImageRule", 76 | "selector": "img", 77 | "properties": { 78 | "image.url": { 79 | "type": "string", 80 | "selector": "img", 81 | "attribute": "src" 82 | } 83 | } 84 | }, { 85 | "class": "ImageRule", 86 | "selector": "//a[img and not(*[not(self::img)])]", 87 | "properties": { 88 | "image.url": { 89 | "type": "string", 90 | "selector": "img", 91 | "attribute": "src" 92 | } 93 | } 94 | }, { 95 | "class": "ListItemRule", 96 | "selector" : "li" 97 | }, { 98 | "class": "ListElementRule", 99 | "selector" : "ul" 100 | }, { 101 | "class": "ListElementRule", 102 | "selector" : "ol" 103 | }, { 104 | "class": "BlockquoteRule", 105 | "selector" : "blockquote" 106 | }, { 107 | "class": "H1Rule", 108 | "selector" : "h1", 109 | "properties" : { 110 | "h1.class" : { 111 | "type" : "string", 112 | "selector" : "link", 113 | "attribute": "class" 114 | } 115 | } 116 | }, { 117 | "class": "H1Rule", 118 | "selector" : "title" 119 | }, { 120 | "class": "H2Rule", 121 | "selector" : "h2", 122 | "properties" : { 123 | "h2.class" : { 124 | "type" : "string", 125 | "selector" : "link", 126 | "attribute": "class" 127 | } 128 | } 129 | }, { 130 | "class": "H2Rule", 131 | "selector" : "h3,h4,h5,h6" 132 | }, { 133 | "class": "InteractiveRule", 134 | "selector" : "blockquote.instagram-media", 135 | "properties" : { 136 | "interactive.iframe" : { 137 | "type" : "multiple", 138 | "children": [ 139 | { 140 | "type": "element", 141 | "selector": "blockquote" 142 | }, { 143 | "type": "next-sibling-element-of", 144 | "selector": "blockquote" 145 | } 146 | ] 147 | } 148 | } 149 | }, { 150 | "class": "InteractiveRule", 151 | "selector" : "blockquote.twitter-tweet", 152 | "properties" : { 153 | "interactive.iframe" : { 154 | "type" : "multiple", 155 | "children": [ 156 | { 157 | "type": "element", 158 | "selector": "blockquote" 159 | }, { 160 | "type": "next-sibling-element-of", 161 | "selector": "blockquote" 162 | } 163 | ] 164 | } 165 | } 166 | }, { 167 | "class": "IgnoreRule", 168 | "selector" : "script" 169 | }, { 170 | "class": "InteractiveRule", 171 | "selector" : "iframe", 172 | "properties" : { 173 | "interactive.url" : { 174 | "type" : "string", 175 | "selector" : "iframe", 176 | "attribute": "src" 177 | }, 178 | "interactive.width" : { 179 | "type" : "int", 180 | "selector" : "iframe", 181 | "attribute": "width" 182 | }, 183 | "interactive.height" : { 184 | "type" : "int", 185 | "selector" : "iframe", 186 | "attribute": "height" 187 | }, 188 | "interactive.iframe" : { 189 | "type" : "children", 190 | "selector" : "iframe" 191 | } 192 | } 193 | }, { 194 | "class": "InteractiveRule", 195 | "selector" : "div.embed", 196 | "properties" : { 197 | "interactive.iframe" : { 198 | "type" : "children", 199 | "selector" : "div.embed" 200 | }, 201 | "interactive.height" : { 202 | "type" : "int", 203 | "selector" : "iframe", 204 | "attribute": "height" 205 | }, 206 | "interactive.width" : { 207 | "type" : "int", 208 | "selector" : "iframe", 209 | "attribute": "width" 210 | } 211 | } 212 | }, { 213 | "class": "InteractiveRule", 214 | "selector" : "div.interactive", 215 | "properties" : { 216 | "interactive.iframe" : { 217 | "type" : "children", 218 | "selector" : "div.interactive" 219 | }, 220 | "interactive.height" : { 221 | "type" : "int", 222 | "selector" : "iframe", 223 | "attribute": "height" 224 | }, 225 | "interactive.width" : { 226 | "type" : "int", 227 | "selector" : "iframe", 228 | "attribute": "width" 229 | } 230 | } 231 | }, { 232 | "class": "InteractiveRule", 233 | "selector" : "//div[@class='embed' and iframe]", 234 | "properties" : { 235 | "interactive.url" : { 236 | "type" : "string", 237 | "selector" : "iframe", 238 | "attribute": "src" 239 | }, 240 | "interactive.iframe" : { 241 | "type" : "children", 242 | "selector" : "iframe", 243 | "attribute": "src" 244 | }, 245 | "interactive.width" : { 246 | "type" : "int", 247 | "selector" : "iframe", 248 | "attribute": "width" 249 | }, 250 | "interactive.height" : { 251 | "type" : "int", 252 | "selector" : "iframe", 253 | "attribute": "height" 254 | } 255 | } 256 | }, { 257 | "class": "InteractiveRule", 258 | "selector" : "//div[@class='interactive' and iframe]", 259 | "properties" : { 260 | "interactive.url" : { 261 | "type" : "string", 262 | "selector" : "iframe", 263 | "attribute": "src" 264 | }, 265 | "interactive.iframe" : { 266 | "type" : "children", 267 | "selector" : "iframe" 268 | }, 269 | "interactive.height" : { 270 | "type" : "int", 271 | "selector" : "iframe", 272 | "attribute": "height" 273 | }, 274 | "interactive.width" : { 275 | "type" : "int", 276 | "selector" : "iframe", 277 | "attribute": "width" 278 | } 279 | } 280 | }, { 281 | "class": "InteractiveRule", 282 | "selector" : "table", 283 | "properties" : { 284 | "interactive.iframe" : { 285 | "type" : "element", 286 | "selector" : "table" 287 | }, 288 | "interactive.height" : { 289 | "type" : "int", 290 | "selector" : "table", 291 | "attribute": "height" 292 | }, 293 | "interactive.width" : { 294 | "type" : "int", 295 | "selector" : "iframe", 296 | "attribute": "width" 297 | } 298 | } 299 | }, { 300 | "class": "SlideshowImageRule", 301 | "selector" : "figure", 302 | "properties" : { 303 | "image.url" : { 304 | "type" : "string", 305 | "selector" : "img", 306 | "attribute": "src" 307 | }, 308 | "caption.title" : { 309 | "type" : "string", 310 | "selector" : "figcaption" 311 | } 312 | } 313 | }, { 314 | "class": "SlideshowRule", 315 | "selector" : "div.gallery" 316 | }, { 317 | "class": "CaptionRule", 318 | "selector" : "figcaption" 319 | }, { 320 | "class": "CaptionRule", 321 | "selector" : "p.wp-caption-text" 322 | }, { 323 | "class": "ImageRule", 324 | "selector" : "figure", 325 | "properties" : { 326 | "image.url" : { 327 | "type" : "string", 328 | "selector" : "img", 329 | "attribute": "src" 330 | } 331 | } 332 | }, { 333 | "class": "SlideshowRule", 334 | "selector" : "figure.wp-block-gallery" 335 | }, { 336 | "class": "PassThroughRule", 337 | "selector" : "ul.blocks-gallery-grid" 338 | }, { 339 | "class": "PassThroughRule", 340 | "selector" : "li.blocks-gallery-item" 341 | }, { 342 | "class": "VideoRule", 343 | "selector" : "div.wp-video", 344 | "containsChild": "video", 345 | "properties" : { 346 | "video.url" : { 347 | "type" : "string", 348 | "selector" : "source", 349 | "attribute": "src" 350 | }, 351 | "video.type" : { 352 | "type" : "string", 353 | "selector" : "source", 354 | "attribute": "type" 355 | } 356 | } 357 | }, { 358 | "class" : "PassThroughRule", 359 | "selector" : "div.vce-gallery-big" 360 | }, { 361 | "class" : "PassThroughRule", 362 | "selector" : "div.vce-gallery-small" 363 | }, { 364 | "class": "PassthroughRule", 365 | "selector": "div.wp-block-columns, div.wp-block-column" 366 | }, { 367 | "class" : "IgnoreRule", 368 | "selector" : "div.vce-gallery-slider" 369 | }, { 370 | "class": "SlideshowImageRule", 371 | "selector" : "div.big-gallery-item", 372 | "properties" : { 373 | "image.url" : { 374 | "type" : "string", 375 | "selector" : "img", 376 | "attribute": "src" 377 | }, 378 | "caption.title" : { 379 | "type" : "string", 380 | "selector" : "figcaption.wp-caption-text" 381 | } 382 | } 383 | } 384 | ] 385 | } 386 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | red=`tput setaf 1` 3 | green=`tput setaf 2` 4 | yellow=`tput setaf 3` 5 | blue=`tput setaf 4` 6 | reset=`tput sgr0` 7 | me='./'`basename "$0"` 8 | 9 | #-------------------------------- 10 | # Functions 11 | #-------------------------------- 12 | function show_help { 13 | cat <] [version]${reset} 17 | 18 | ${green}Arguments:${reset} 19 | version - The target version (ex: 3.2.1) 20 | 21 | ${green}Options:${reset} 22 | -h Display this help message 23 | -v Verbose mode 24 | -s Simulate only (do not release) 25 | -c Runs only a single command. Possible commands are: 26 | - bump_version: generate a new version tag on the repository 27 | - release: release a new version on GitHub 28 | - publish: publishes the target version to the WordPress 29 | plugin repository 30 | 31 | 32 | ${green}Examples:${reset} 33 | 34 | ${blue}${me} 3.3.0${reset} 35 | Runs bump_version then release for 3.3.0. This is the default use case. 36 | 37 | ${blue}${me} -c bump_version 3.3.0${reset} 38 | Generates a new commit on master changing the version to 3.3.0 in 39 | all relevant files, tags the commit and pushes to remote. 40 | 41 | ${blue}${me} -c release 3.3.0${reset} 42 | Creates a new Release on GitHub based on the tag 3.3.0 and uploads 43 | the binary package based on master. 44 | ${red}IMPORTANT: this will create a new tag if tag 3.3.0 doesn't exist, 45 | so make sure to bump_version beforehand.${reset} 46 | 47 | ${blue}${me} -v 3.3.0${reset} 48 | Releases 3.3.0 in verbose mode. 49 | 50 | ${blue}${me} -s 3.3.0${reset} 51 | Simulates a 3.3.0 release: prints the commands instead of running them. 52 | 53 | EOF 54 | } 55 | 56 | function invalid_usage { 57 | printf $red 58 | echo $@ 59 | echo "Aborted" 60 | printf $reset 61 | show_help 62 | exit -1; 63 | } 64 | function error_message { 65 | printf $red 66 | echo $@ 67 | echo "Aborted" 68 | printf $reset 69 | exit -1 70 | } 71 | function message { 72 | if [[ $verbose == 1 ]]; then 73 | printf $green 74 | echo $@ 75 | printf $reset 76 | fi 77 | } 78 | function run_message { 79 | if [[ $simulate == 1 ]]; then 80 | printf $yellow 81 | echo $@ 82 | printf $reset 83 | fi 84 | } 85 | 86 | #---------------- 87 | # Read parameters 88 | #---------------- 89 | 90 | # A POSIX variable 91 | # Reset in case getopts has been used previously in the shell. 92 | OPTIND=1 93 | 94 | # Read options: 95 | verbose=0 96 | simulate=0 97 | selected_cmd='all' 98 | 99 | while getopts "hvc:s" opt; do 100 | case "$opt" in 101 | h|\?) 102 | show_help 103 | exit 0 104 | ;; 105 | v) verbose=1 106 | ;; 107 | s) simulate=1 108 | ;; 109 | c) selected_cmd="${OPTARG}" 110 | ;; 111 | esac 112 | done 113 | 114 | shift $((OPTIND-1)) 115 | 116 | # Read argument 117 | version=$1 118 | 119 | # Validates arguments 120 | if [[ $2 ]]; then 121 | invalid_usage "Invalid parameters" 122 | fi 123 | 124 | if [[ ! $( echo $version | grep -Ee '^[0-9]+\.[0-9]+(\.[0-9]+)?$' ) ]]; then 125 | invalid_usage "Invalid version provided" 126 | fi 127 | 128 | message "Releasing version: $version" 129 | message "Running in verbose mode" 130 | if [[ $simulate == 1 ]]; then 131 | message "Running in simulation mode" 132 | fi 133 | 134 | #--------------------------------- 135 | # Check if we have the right tools 136 | #--------------------------------- 137 | 138 | if ! type "git" > /dev/null; then 139 | error_message "git not found, please install git before continuing: http://git.org" 140 | else 141 | message "Found git: $(git --version)" 142 | fi 143 | 144 | if ! type "js" > /dev/null; then 145 | error_message "SpiderMonkey interpreter not found, please install SpiderMonkey before continuing: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey (or with Homebrew)" 146 | else 147 | message "Found SpiderMonkey" 148 | fi 149 | 150 | 151 | if ! type "github-changes" > /dev/null; then 152 | error_message "github-changes tool not found. Please run: npm install -g github-changes" 153 | else 154 | message "Found github-changes" 155 | fi 156 | 157 | #------------------------------------ 158 | # Check if we are in the right folder 159 | #------------------------------------ 160 | if [[ ! -e '.git/config' ]]; then 161 | error_message "You should run this command from the root directory of your repository." 162 | fi 163 | if [[ ! $( cat .git/config | grep -i 'automattic/facebook-instant-articles-wp') ]]; then 164 | error_message "You should run this command from the root directory of the facebook-instant-articles-wp repository." 165 | fi 166 | 167 | repo_dir=$(pwd) 168 | 169 | #------------------- 170 | # Manages simulation 171 | #------------------- 172 | function run { 173 | if [[ $simulate == 1 ]]; then 174 | run_message $@ 175 | else 176 | "$@" 177 | fi 178 | } 179 | 180 | function revert_repo { 181 | run cd $repo_dir 182 | if [[ $branch_name != 'master' ]]; then 183 | message "Going back to $branch_name" 184 | # stashes anything possibly left from the script execution 185 | run git stash 186 | run git checkout $branch_name 187 | fi 188 | if [[ $stash_ref ]]; then 189 | message "Applying stashed changes" 190 | run git stash apply $stash_ref 191 | fi 192 | } 193 | 194 | function confirm { 195 | confirm='' 196 | while [[ $confirm != 'a' && $confirm != 'y' ]]; do 197 | printf $blue 198 | printf "%b" "$*" 199 | printf ' (y)es/(a)bort: ' 200 | printf $red 201 | read -n 1 confirm 202 | printf "\n" 203 | done 204 | if [[ $confirm != 'y' ]]; then 205 | revert_repo 206 | error_message 'Execution aborted by the user' 207 | exit -1 208 | fi 209 | printf $reset 210 | } 211 | 212 | function ask { 213 | user_response='' 214 | while [[ $user_response != 'n' && $user_response != 'y' ]]; do 215 | printf $blue 216 | printf "%b" "$*" 217 | printf ' (y)es/(n)o: ' 218 | printf $red 219 | read -n 1 user_response 220 | printf "\n" 221 | done 222 | printf $reset 223 | } 224 | 225 | function prompt { 226 | user_response='' 227 | printf $blue 228 | printf "%b" "$*" 229 | printf $red 230 | read user_response 231 | printf $reset 232 | } 233 | 234 | function prompt_password { 235 | user_response='' 236 | printf $blue 237 | printf "%b" "$*" 238 | printf $red 239 | read -s user_response 240 | printf $reset 241 | } 242 | 243 | #---------------------- 244 | # Commands 245 | #---------------------- 246 | function bump_version { 247 | 248 | message "Stashing current work..." 249 | 250 | stash_ref=$(git stash create) 251 | run_message git stash create 252 | 253 | if [[ $stash_ref ]]; then 254 | run git reset --hard 255 | message "Stashed current work to: $stash_ref" 256 | else 257 | message "Nothing to stash" 258 | fi 259 | 260 | branch_name="$(git symbolic-ref HEAD 2>/dev/null)" 261 | branch_name=${branch_name##refs/heads/} 262 | message "Current branch: $branch_name" 263 | 264 | if [[ $branch_name != 'master' ]]; then 265 | message "Switching to master..." 266 | run git checkout master 267 | fi 268 | 269 | message "Pulling latest version from GitHub" 270 | run git pull --rebase 271 | 272 | confirm "Replace stable tag on readme.txt with $version?" 273 | message "Replacing stable tag on readme.txt" 274 | run sed -i -e "s/Stable tag: .*/Stable tag: $version/" ./readme.txt 275 | run git diff 276 | confirm "Add changes to commit?" 277 | run git add readme.txt 278 | run rm readme.txt-e 279 | 280 | confirm "Replace version on facebook-instant-articles-wp.php with $version?" 281 | message "Replacing version on facebook-instant-articles-wp.php" 282 | run sed -i -e "s/^ \* Version: .*/ * Version: $version/" facebook-instant-articles.php 283 | run sed -i -e "s/define( 'IA_PLUGIN_VERSION', '[0-9.]*' );/define( 'IA_PLUGIN_VERSION', '$version' );/" facebook-instant-articles.php 284 | run git diff 285 | confirm "Add changes to commit?" 286 | run git add facebook-instant-articles.php 287 | run rm facebook-instant-articles.php-e 288 | 289 | confirm "Update CHANGELOG.md for $version?" 290 | message "Updating CHANGELOG.md for $version" 291 | run github-changes -o automattic -r facebook-instant-articles-wp -a --only-pulls --use-commit-body --tag-name $version 292 | run git diff 293 | confirm "Add changes to commit?" 294 | run git add CHANGELOG.md 295 | 296 | confirm "Update changelog on readme.txt?" 297 | message "Updating changelog on readme.txt" 298 | run sed '/== Changelog ==/q' ./readme.txt >> ./readme2.txt 299 | run cat ./CHANGELOG.md >> ./readme2.txt 300 | run rm ./readme.txt 301 | run mv ./readme2.txt ./readme.txt 302 | run git diff 303 | confirm "Add changes to commit?" 304 | run git add readme.txt 305 | 306 | confirm "Commit version bump on master with message 'Bump version to $version'?" 307 | run git commit -m "Bump version to $version" 308 | 309 | confirm "Create tag $version?" 310 | run git tag $version 311 | 312 | confirm "Push tag and commit to GitHub?" 313 | run git push 314 | run git push --tags 315 | 316 | revert_repo 317 | 318 | echo "🍺 Tag $version created!" 319 | } 320 | 321 | function release { 322 | 323 | confirm "Create a new release for $version?" 324 | 325 | message "Stashing current work..." 326 | 327 | stash_ref=$(git stash create) 328 | run_message git stash create 329 | 330 | if [[ $stash_ref ]]; then 331 | run git reset --hard 332 | message "Stashed current work to: $stash_ref" 333 | else 334 | message "Nothing to stash" 335 | fi 336 | 337 | branch_name="$(git symbolic-ref HEAD 2>/dev/null)" 338 | branch_name=${branch_name##refs/heads/} 339 | message "Current branch: $branch_name" 340 | 341 | if [[ $branch_name != 'master' ]]; then 342 | message "Switching to master..." 343 | run git checkout master 344 | fi 345 | 346 | message "Pulling latest version from GitHub" 347 | run git pull --rebase 348 | 349 | if [[ ! -e resty ]]; then 350 | message "Downloading resty to connect to GitHub API" 351 | run curl -L http://github.com/micha/resty/raw/2.2/resty > resty 352 | fi 353 | if [[ ! -e jsawk ]]; then 354 | message "Downloading jsawk to parse info from GitHub API" 355 | run curl -L http://github.com/micha/jsawk/raw/1.4/jsawk > jsawk 356 | fi 357 | 358 | prompt "GitHub access-token (required only for 2fac):" 359 | github_access_token=$user_response 360 | 361 | if [[ github_access_token ]]; then 362 | run . resty -W 'https://api.github.com' -H "Authorization: token $github_access_token" 363 | else 364 | prompt "GitHub username:" 365 | github_username=$user_response 366 | 367 | prompt_password "GitHub password:" 368 | github_password=$user_response 369 | 370 | run . resty -W 'https://api.github.com' -u $github_username:$github_password 371 | fi 372 | 373 | response=$(run POST /repos/Automattic/facebook-instant-articles-wp/releases " 374 | { 375 | \"tag_name\": \"$version\", 376 | \"target_commitish\": \"master\", 377 | \"name\": \"$version\", 378 | \"body\": \"Version $version\", 379 | \"draft\": false, 380 | \"prerelease\": false 381 | } 382 | "); 383 | 384 | if [[ $response ]]; then 385 | message "Release $version created!" 386 | else 387 | error_message "Couldn't create release" 388 | fi 389 | 390 | upload_url=$( echo $response | . jsawk -n 'out(this.upload_url)' | sed -e "s/{[^}]*}//g" ) 391 | release_id=$( echo $response | . jsawk -n 'out(this.id)' ) 392 | 393 | message "Upload URL: $upload_url" 394 | 395 | message "Creating binary file" 396 | run composer install --no-dev 397 | run zip -qr facebook-instant-articles-wp.zip . 398 | 399 | message "Uploading binary for release..." 400 | 401 | if [[ github_access_token ]]; then 402 | response=$(run curl -H "Authorization: token $github_access_token" -H "Content-Type: application/zip" --data-binary @facebook-instant-articles-wp.zip $upload_url\?name=facebook-instant-articles-wp-$version.zip ) 403 | else 404 | response=$(run curl -u $github_username:$github_password -H "Content-Type: application/zip" --data-binary @facebook-instant-articles-wp.zip $upload_url\?name=facebook-instant-articles-wp-$version.zip ) 405 | fi 406 | 407 | run rm facebook-instant-articles-wp.zip 408 | revert_repo 409 | 410 | if [[ $response ]]; then 411 | echo "🍺 Release $version successfully created" 412 | else 413 | error_message "Couldn't upload file" 414 | fi 415 | 416 | 417 | } 418 | 419 | function publish { 420 | confirm "Publish $version to WordPress plugin repository?" 421 | 422 | message "Stashing current work..." 423 | 424 | stash_ref=$(git stash create) 425 | run_message git stash create 426 | 427 | if [[ $stash_ref ]]; then 428 | run git reset --hard 429 | message "Stashed current work to: $stash_ref" 430 | else 431 | message "Nothing to stash" 432 | fi 433 | 434 | branch_name="$(git symbolic-ref HEAD 2>/dev/null)" 435 | branch_name=${branch_name##refs/heads/} 436 | message "Current branch: $branch_name" 437 | 438 | if [[ $branch_name != 'master' ]]; then 439 | message "Switching to master..." 440 | run git checkout master 441 | fi 442 | 443 | message "Pulling latest version from GitHub" 444 | run git pull --rebase 445 | 446 | tmp_dir=$(mktemp -d) 447 | 448 | message "Updating composer dependencies" 449 | run composer install --no-dev 450 | 451 | message "Checking out SVN repository..." 452 | run cd $tmp_dir 453 | run svn checkout -q https://plugins.svn.wordpress.org/fb-instant-articles/ 454 | run cd fb-instant-articles/trunk/ 455 | 456 | confirm "Copy new version to trunk?" 457 | run cp -rf $repo_dir/* ./ 458 | 459 | # Removes development files we know shouldn't make to the SVN repo 460 | run rm -rf .[!.]* # this will remove all hidden files 461 | run rm -rf bin 462 | run rm -rf tests 463 | run rm -rf composer* 464 | run rm -rf phpunit* 465 | run rm -rf vendor/apache/log4php/src/test 466 | run rm -rf facebook-instant-articles-wp.zip 467 | run rm -rf jsawk 468 | run rm -rf resty 469 | run rm -rf vendor/apache/log4php/.git 470 | 471 | run svn st | grep '^\?' | sed 's/^\? *//' | xargs -I% svn add % 472 | run svn status 473 | ask "Review changes?" 474 | if [[ $user_response == 'y' ]]; then 475 | run svn diff 476 | fi 477 | confirm "Commit changes to trunk?" 478 | run svn commit -m "Release $version" 479 | confirm "Tag version $version?" 480 | run svn cp ../trunk ../tags/$version 481 | run cd .. 482 | run svn commit -m "Tag $version" 483 | 484 | revert_repo 485 | 486 | echo "🍺 Published $version successfully" 487 | } 488 | 489 | # Run right command 490 | if [[ $selected_cmd == 'bump_version' ]]; then bump_version; exit 0; fi 491 | if [[ $selected_cmd == 'release' ]]; then release; exit 0; fi 492 | if [[ $selected_cmd == 'publish' ]]; then publish; exit 0; fi 493 | if [[ $selected_cmd == 'all' ]]; then bump_version; release; publish; exit 0; fi 494 | error_message "Invalid command $selected_cmd" 495 | -------------------------------------------------------------------------------- /src/wizard/class-instant-articles-option.php: -------------------------------------------------------------------------------- 1 | ) an Array containing keys of all disabled `select_options` can be defined. Defaults to false. 53 | * serialized_with_group - String|false/null. Controls whether a particular field of a setting is serialized with the group or saved as its own independant option. Defaults to true. 54 | * label - String|null. The label for the field. 55 | * description - String. The description for the field, rendered as additional information below the field. 56 | * render - String|Function. How the field should be rendered as. Non-strings are assumed to be a function for rendering the field (third parameter of `add_settings_field()`). If a String, it is meant to be any of the standard
    input types (checkbox, radio, textarea, hidden, select, textarea, password etc.) and the rendering will be handled by self::universal_render_handler(); if not defined, "text" is assumed 57 | * select_options - Array. Defines the for a checkbox (key of this Array is used as its `value` attribute). Only used if the `render` is "select". 58 | * radio_options - Array. TO BE IMPLEMENTED (should mimic the "select_options" functionality) 59 | * placeholder - String. Used as the `placeholder` attribute for the field. 60 | * default - String|null. Used as a default value for the field when there is no value yet saved in the db. 61 | * 62 | * @var array $fields The fields for option and its properties. 63 | * @since 0.4 64 | */ 65 | private $field_definitions; 66 | 67 | /** 68 | * If $option_group is not specified, the Option will be registered with a 69 | * "default" page group (the first argument of `register_setting()` and called 70 | * by `settings_fields()`). 71 | * 72 | * @param string $option_key The ID for the field. 73 | * @param array $sections The sections for each field. 74 | * @param array $option_fields All the fields. 75 | * @param array $option_group Optional, if not informed will use self::PAGE_OPTION_GROUP. 76 | * @since 0.4 77 | */ 78 | public function __construct( $option_key, $sections, $option_fields, $option_group = null ) { 79 | $this->key = $option_key; 80 | $this->sections = $sections; 81 | $this->field_definitions = $option_fields; 82 | $this->page_option_group = $option_group ?? self::PAGE_OPTION_GROUP; 83 | 84 | $this->init(); 85 | } 86 | 87 | /** 88 | * Option initiator. 89 | * 90 | * @since 0.4 91 | */ 92 | private function init() { 93 | $saved_options = self::get_option_decoded( $this->key ); 94 | 95 | foreach ( $this->field_definitions as $field_key => $field ) { 96 | self::$settings[ $field_key ] = $saved_options[ $field_key ] ?? $field['default']; 97 | } 98 | 99 | $this->wp_bootstrap_register_option(); 100 | $this->wp_bootstrap_create_sections(); 101 | $this->wp_bootstrap_add_fields_to_section(); 102 | } 103 | 104 | /** 105 | * Decodes the option. 106 | * 107 | * @param string $option_key to be returned. 108 | * @return array from a json decoded content. 109 | * @since 0.4 110 | */ 111 | public static function get_option_decoded( $option_key = null ) { 112 | if ( ! isset( $option_key ) ) { 113 | // Late Static Binding to use the const OPTION_KEY from the child class which called this function. 114 | $option_key = static::OPTION_KEY; 115 | } 116 | 117 | $raw_data = get_option( $option_key ); 118 | 119 | // Hack which creates an empty setting if it doesn't yet exist. 120 | // Temporary solution to an unknown oddity which is double-escaping the JSON 121 | // data as a string when attempting to access a setting that doesn't exist. 122 | if ( false === $raw_data ) { 123 | add_option( $option_key ); 124 | $raw_data = get_option( $option_key ); 125 | } 126 | 127 | return json_decode( $raw_data, true ); 128 | } 129 | 130 | /** 131 | * Obtains the compat related to $action_tag. 132 | * 133 | * @param string $action_tag The tag registered compat will be retrieved. 134 | * @since 0.4 135 | */ 136 | public static function get_registered_compat( $action_tag ) { 137 | $registered_compat_integrations = array(); 138 | 139 | do_action_ref_array( 140 | $action_tag, 141 | array( &$registered_compat_integrations ) 142 | ); 143 | return $registered_compat_integrations; 144 | } 145 | 146 | /** 147 | * Registers the sanitization and encoding handler. 148 | * 149 | * @since 0.4 150 | */ 151 | private function wp_bootstrap_register_option() { 152 | register_setting( 153 | $this->page_option_group, 154 | $this->key, 155 | array( $this, 'universal_sanitize_and_encode_handler' ) 156 | ); 157 | } 158 | 159 | /** 160 | * Create title and description sections. 161 | * 162 | * @since 0.4 163 | */ 164 | private function wp_bootstrap_create_sections() { 165 | $title = $this->sections['title'] ?? ''; 166 | 167 | $description = isset( $this->sections['description'] ) 168 | ? wp_kses( 169 | $this->sections['description'], 170 | array( 171 | 'a' => array( 172 | 'href' => array(), 173 | 'target' => array(), 174 | ), 175 | 'em' => array(), 176 | 'p' => array(), 177 | 'strong' => array(), 178 | ) 179 | ) 180 | : ''; 181 | 182 | add_settings_section( 183 | $this->key, 184 | esc_html( $title ), 185 | static function () use ( $description ) { 186 | echo wp_kses_post( $description ); 187 | }, 188 | $this->key 189 | ); 190 | } 191 | 192 | /** 193 | * Add fields to defined section. 194 | * 195 | * @since 0.4 196 | */ 197 | private function wp_bootstrap_add_fields_to_section() { 198 | foreach ( $this->field_definitions as $field_key => $field ) { 199 | $standalone_id = $this->key . '-' . $field_key; 200 | 201 | // Default values of arguments for renderer. 202 | $renderer_args = array( 203 | // The "label_for" arg causes WordPress to wrap the label of the field with a