Ads.txt Manager - Version 1.2.0

Version Description

  • Added: Make revisions accessible in the admin - now you can restore older versions of your ads.txt or view how it's changed over time (props @adamsilverstein, @helen)
  • Added: Show a notice on the edit screen if an ads.txt file exists on the server (props @kkoppenhaver, @helen, @tomjn, @adamsilverstein)
  • Added: Add a custom edit_ads_txt capability for granular assignment, which is assigned to administrators by default (props @ethanclevenger91, @adamsilverstein)
  • Added: Enable filtering of the output using ads_txt_content (props @ethanclevenger91)
  • Changed: Updated documentation, automation, and coding standards (props @jeffpaul, @adamsilverstein, @helen, @mmcachran)
  • Fixed: Early escaping (props @tomjn)
  • Fixed: PHPCS issues and added PHPCS scanning (props @adamsilverstein)
Download this release

Release Info

Developer helen
Plugin Icon 128x128 Ads.txt Manager
Version 1.2.0
Comparing to
See all releases

Code changes from version 1.0 to 1.2.0

Files changed (7) hide show
  1. ads-txt.php +58 -5
  2. css/admin.css +3 -0
  3. inc/admin.php +273 -42
  4. inc/post-type.php +10 -4
  5. inc/save.php +39 -37
  6. js/admin.js +36 -5
  7. readme.txt +26 -4
ads-txt.php CHANGED
@@ -2,17 +2,23 @@
2
  /**
3
  * Plugin Name: Ads.txt Manager
4
  * Description: Create, manage, and validate your Ads.txt from within WordPress, just like any other content asset. Requires PHP 5.3+ and WordPress 4.9+.
5
- * Version: 1.0
6
  * Author: 10up
7
- * Author URI: http://10up.com
8
  * License: GPLv2 or later
9
  * Text Domain: ads-txt
 
 
10
  */
11
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // Exit if accessed directly.
14
  }
15
 
 
 
 
 
16
  require_once __DIR__ . '/inc/post-type.php';
17
  require_once __DIR__ . '/inc/admin.php';
18
  require_once __DIR__ . '/inc/save.php';
@@ -23,17 +29,64 @@ require_once __DIR__ . '/inc/save.php';
23
  * @return void
24
  */
25
  function tenup_display_ads_txt() {
26
- $request = esc_url_raw( $_SERVER['REQUEST_URI'] );
27
  if ( '/ads.txt' === $request ) {
28
- $post_id = get_option( 'adstxt_post' );
29
 
30
  // Will fall through if no option found, likely to a 404.
31
  if ( ! empty( $post_id ) ) {
32
  $post = get_post( $post_id );
33
  header( 'Content-Type: text/plain' );
34
- echo esc_html( $post->post_content );
 
 
 
 
 
 
 
 
 
35
  die();
36
  }
37
  }
38
  }
39
  add_action( 'init', 'tenup_display_ads_txt' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  /**
3
  * Plugin Name: Ads.txt Manager
4
  * Description: Create, manage, and validate your Ads.txt from within WordPress, just like any other content asset. Requires PHP 5.3+ and WordPress 4.9+.
5
+ * Version: 1.2.0
6
  * Author: 10up
7
+ * Author URI: https://10up.com
8
  * License: GPLv2 or later
9
  * Text Domain: ads-txt
10
+ *
11
+ * @package Ads_Txt_Manager
12
  */
13
 
14
  if ( ! defined( 'ABSPATH' ) ) {
15
  exit; // Exit if accessed directly.
16
  }
17
 
18
+ define( 'ADS_TXT_MANAGER_VERSION', '1.2.0' );
19
+ define( 'ADS_TXT_MANAGE_CAPABILITY', 'edit_ads_txt' );
20
+ define( 'ADS_TXT_MANAGER_POST_OPTION', 'adstxt_post' );
21
+
22
  require_once __DIR__ . '/inc/post-type.php';
23
  require_once __DIR__ . '/inc/admin.php';
24
  require_once __DIR__ . '/inc/save.php';
29
  * @return void
30
  */
31
  function tenup_display_ads_txt() {
32
+ $request = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : false;
33
  if ( '/ads.txt' === $request ) {
34
+ $post_id = get_option( ADS_TXT_MANAGER_POST_OPTION );
35
 
36
  // Will fall through if no option found, likely to a 404.
37
  if ( ! empty( $post_id ) ) {
38
  $post = get_post( $post_id );
39
  header( 'Content-Type: text/plain' );
40
+ $adstxt = $post->post_content;
41
+
42
+ /**
43
+ * Filter the ads.txt content.
44
+ *
45
+ * @since 1.2.0
46
+ *
47
+ * @param type $adstxt The existing ads.txt content.
48
+ */
49
+ echo esc_html( apply_filters( 'ads_txt_content', $adstxt ) );
50
  die();
51
  }
52
  }
53
  }
54
  add_action( 'init', 'tenup_display_ads_txt' );
55
+
56
+ /**
57
+ * Add custom capabilities.
58
+ *
59
+ * @return void
60
+ */
61
+ function add_adstxt_capabilities() {
62
+ $role = get_role( 'administrator' );
63
+ if ( ! $role->has_cap( ADS_TXT_MANAGE_CAPABILITY ) ) {
64
+ $role->add_cap( ADS_TXT_MANAGE_CAPABILITY );
65
+ }
66
+ }
67
+ add_action( 'admin_init', 'add_adstxt_capabilities' );
68
+ register_activation_hook( __FILE__, 'add_adstxt_capabilities' );
69
+
70
+ /**
71
+ * Remove custom capabilities when deactivating the plugin.
72
+ *
73
+ * @return void
74
+ */
75
+ function remove_adstxt_capabilities() {
76
+ $role = get_role( 'administrator' );
77
+ $role->remove_cap( ADS_TXT_MANAGE_CAPABILITY );
78
+ }
79
+ register_deactivation_hook( __FILE__, 'remove_adstxt_capabilities' );
80
+
81
+ /**
82
+ * Add a query var to detect when ads.txt has been saved.
83
+ *
84
+ * @param array $qvars Array of query vars.
85
+ *
86
+ * @return array Array of query vars.
87
+ */
88
+ function tenup_ads_txt_add_query_vars( $qvars ) {
89
+ $qvars[] = 'ads_txt_saved';
90
+ return $qvars;
91
+ }
92
+ add_filter( 'query_vars', 'tenup_ads_txt_add_query_vars' );
css/admin.css ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ .adstxt-revision-count {
2
+ font-weight: bold;
3
+ }
inc/admin.php CHANGED
@@ -1,4 +1,9 @@
1
  <?php
 
 
 
 
 
2
 
3
  namespace AdsTxt;
4
 
@@ -14,13 +19,24 @@ function admin_enqueue_scripts( $hook ) {
14
  return;
15
  }
16
 
17
- wp_enqueue_script( 'adstxt', plugins_url( '/js/admin.js', dirname( __FILE__ ) ), array( 'jquery', 'wp-backbone', 'wp-codemirror' ), false, true );
 
 
 
 
 
 
18
  wp_enqueue_style( 'code-editor' );
 
 
 
 
 
 
19
 
20
  $strings = array(
21
- 'saved_message' => __( 'Ads.txt saved', 'ads-txt' ),
22
- 'error_message' => __( 'Your Ads.txt contains the following issues:', 'ads-txt' ),
23
- 'unknown_error' => __( 'An unknown error occurred.', 'ads-txt' ),
24
  );
25
 
26
  wp_localize_script( 'adstxt', 'adstxt', $strings );
@@ -35,7 +51,7 @@ add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\admin_enqueue_scripts' );
35
  * @return void
36
  */
37
  function admin_head_css() {
38
- ?>
39
  <style>
40
  .CodeMirror {
41
  width: 100%;
@@ -43,19 +59,96 @@ function admin_head_css() {
43
  height: calc( 100vh - 295px );
44
  border: 1px solid #ddd;
45
  box-sizing: border-box;
46
- }
47
  </style>
48
- <?php
49
  }
50
  add_action( 'admin_head-settings_page_adstxt-settings', __NAMESPACE__ . '\admin_head_css' );
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  /**
53
  * Add admin menu page.
54
  *
55
  * @return void
56
  */
57
  function admin_menu() {
58
- add_options_page( __( 'Ads.txt', 'ads-txt' ), __( 'Ads.txt', 'ads-txt' ), 'manage_options', 'adstxt-settings', __NAMESPACE__ . '\settings_screen' );
 
 
 
 
 
 
59
  }
60
  add_action( 'admin_menu', __NAMESPACE__ . '\admin_menu' );
61
 
@@ -65,40 +158,113 @@ add_action( 'admin_menu', __NAMESPACE__ . '\admin_menu' );
65
  * @return void
66
  */
67
  function settings_screen() {
68
- $post_id = get_option( 'adstxt_post' );
69
- $post = false;
70
- $content = false;
 
 
 
71
 
72
  if ( $post_id ) {
73
  $post = get_post( $post_id );
74
- $content = isset( $post->post_content ) ? $post->post_content : '';
75
- $errors = get_post_meta( $post->ID, 'adstxt_errors', true );
76
  }
77
- ?>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  <div class="wrap">
79
- <?php if ( ! empty( $errors ) ) : ?>
 
 
 
 
 
 
 
80
  <div class="notice notice-error adstxt-notice">
81
- <p><strong><?php echo esc_html( __( 'Your Ads.txt contains the following issues:', 'ads-txt' ) ); ?></strong></p>
82
  <ul>
83
  <?php
84
  foreach ( $errors as $error ) {
85
- echo '<li class="' . esc_attr( $error['type'] ) . '">' . esc_html( format_error( $error ) ) . '</li>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
  ?>
88
  </ul>
89
  </div>
90
- <?php endif; ?>
91
 
92
- <h2><?php echo esc_html( __( 'Manage Ads.txt', 'ads-txt' ) ); ?></h2>
93
 
94
  <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="adstxt-settings-form">
95
- <input type="hidden" name="post_id" value="<?php echo ( $post ? esc_attr( $post->ID ) : '' ); ?>" />
96
  <input type="hidden" name="action" value="adstxt-save" />
97
  <?php wp_nonce_field( 'adstxt_save' ); ?>
98
 
99
- <label class="screen-reader-text" for="adstxt_content"><?php echo esc_html( __( 'Ads.txt content', 'ads-txt' ) ); ?></label>
100
  <textarea class="widefat code" rows="25" name="adstxt" id="adstxt_content"><?php echo esc_textarea( $content ); ?></textarea>
101
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  <div id="adstxt-notification-area"></div>
103
 
104
  <p class="submit">
@@ -109,19 +275,27 @@ function settings_screen() {
109
  </form>
110
 
111
  <script type="text/template" id="tmpl-adstext-notice">
112
- <# if ( ! _.isUndefined( data.saved ) ) { #>
113
- <div class="notice notice-success adstxt-notice adstxt-saved">
114
- <p>{{ data.saved.saved_message }}</p>
115
- </div>
116
- <# } #>
117
-
118
  <# if ( ! _.isUndefined( data.errors ) ) { #>
119
  <div class="notice notice-error adstxt-notice adstxt-errors">
120
  <p><strong>{{ data.errors.error_message }}</strong></p>
121
  <# if ( ! _.isUndefined( data.errors.errors ) ) { #>
122
  <ul class="adstxt-errors-items">
123
  <# _.each( data.errors.errors, function( error ) { #>
124
- <li>{{ error }}.</li>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  <# } ); #>
126
  </ul>
127
  <# } #>
@@ -131,7 +305,7 @@ function settings_screen() {
131
  <p class="adstxt-ays">
132
  <input id="adstxt-ays-checkbox" name="adstxt_ays" type="checkbox" value="y" />
133
  <label for="adstxt-ays-checkbox">
134
- <?php _e( 'Update anyway, even though it may adversely affect your ads?', 'ads-txt' ); ?>
135
  </label>
136
  </p>
137
  <# } #>
@@ -140,29 +314,86 @@ function settings_screen() {
140
  </script>
141
  </div>
142
 
143
- <?php
144
  }
145
 
146
  /**
147
- * Take an error array and turn it into a message.
148
  *
149
  * @param array $error {
150
  * Array of error message components.
151
  *
152
- * @type string $type Type of error. Typically 'warning' or 'error'.
153
  * @type int $line Line number of the error.
154
- * @type string $message Error message.
 
155
  * }
156
  *
157
- * @return string Formatted error message.
158
  */
159
- function format_error( $error ) {
160
- /* translators: Error message output. 1: Line number, 2: Error message */
161
- $message = sprintf(
162
- __( 'Line %1$s: %2$s', 'ads-txt' ),
163
- $error['line'],
164
- $error['message']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  );
166
 
167
- return $message;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
 
1
  <?php
2
+ /**
3
+ * Admin functionality for Ads.txt.
4
+ *
5
+ * @package Ads_Txt_Manager
6
+ */
7
 
8
  namespace AdsTxt;
9
 
19
  return;
20
  }
21
 
22
+ wp_enqueue_script(
23
+ 'adstxt',
24
+ esc_url( plugins_url( '/js/admin.js', dirname( __FILE__ ) ) ),
25
+ array( 'jquery', 'wp-backbone', 'wp-codemirror' ),
26
+ ADS_TXT_MANAGER_VERSION,
27
+ true
28
+ );
29
  wp_enqueue_style( 'code-editor' );
30
+ wp_enqueue_style(
31
+ 'adstxt',
32
+ esc_url( plugins_url( '/css/admin.css', dirname( __FILE__ ) ) ),
33
+ array(),
34
+ ADS_TXT_MANAGER_VERSION
35
+ );
36
 
37
  $strings = array(
38
+ 'error_message' => esc_html__( 'Your Ads.txt contains the following issues:', 'ads-txt' ),
39
+ 'unknown_error' => esc_html__( 'An unknown error occurred.', 'ads-txt' ),
 
40
  );
41
 
42
  wp_localize_script( 'adstxt', 'adstxt', $strings );
51
  * @return void
52
  */
53
  function admin_head_css() {
54
+ ?>
55
  <style>
56
  .CodeMirror {
57
  width: 100%;
59
  height: calc( 100vh - 295px );
60
  border: 1px solid #ddd;
61
  box-sizing: border-box;
62
+ }
63
  </style>
64
+ <?php
65
  }
66
  add_action( 'admin_head-settings_page_adstxt-settings', __NAMESPACE__ . '\admin_head_css' );
67
 
68
+ /**
69
+ * Appends a query argument to the edit url to make sure it is redirected to
70
+ * the ads.txt screen.
71
+ *
72
+ * @since 1.2.0
73
+ *
74
+ * @param string $url Edit url.
75
+ * @return string Edit url.
76
+ */
77
+ function ads_txt_adjust_revisions_return_to_editor_link( $url ) {
78
+ global $pagenow;
79
+
80
+ if ( 'revision.php' !== $pagenow || ! isset( $_REQUEST['adstxt'] ) ) { // @codingStandardsIgnoreLine Nonce not required.
81
+ return $url;
82
+ }
83
+
84
+ return admin_url( 'options-general.php?page=adstxt-settings' );
85
+ }
86
+ add_filter( 'get_edit_post_link', __NAMESPACE__ . '\ads_txt_adjust_revisions_return_to_editor_link' );
87
+
88
+ /**
89
+ * Modifies revisions data to preserve adstxt argument used in determining
90
+ * where to redirect user returning to editor.
91
+ *
92
+ * @since 1.9.0
93
+ *
94
+ * @param array $revisions_data The bootstrapped data for the revisions screen.
95
+ * @return array Modified bootstrapped data for the revisions screen.
96
+ */
97
+ function adstxt_revisions_restore( $revisions_data ) {
98
+ if ( isset( $_REQUEST['adstxt'] ) ) { // @codingStandardsIgnoreLine Nonce not required.
99
+ $revisions_data['restoreUrl'] = add_query_arg(
100
+ 'adstxt',
101
+ 1,
102
+ $revisions_data['restoreUrl']
103
+ );
104
+ }
105
+
106
+ return $revisions_data;
107
+ }
108
+ add_filter( 'wp_prepare_revision_for_js', __NAMESPACE__ . '\adstxt_revisions_restore' );
109
+
110
+ /**
111
+ * Hide the revisions title with CSS, since WordPress always shows the title
112
+ * field even if unchanged, and the title is not relevant for ads.txt.
113
+ */
114
+ function admin_header_revisions_styles() {
115
+ $current_screen = get_current_screen();
116
+
117
+ if ( ! $current_screen || 'revision' !== $current_screen->id ) {
118
+ return;
119
+ }
120
+
121
+ if ( ! isset( $_REQUEST['adstxt'] ) ) { // @codingStandardsIgnoreLine Nonce not required.
122
+ return;
123
+ }
124
+
125
+ ?>
126
+ <style>
127
+ .revisions-diff .diff h3 {
128
+ display: none;
129
+ }
130
+ .revisions-diff .diff table.diff:first-of-type {
131
+ display: none;
132
+ }
133
+ </style>
134
+ <?php
135
+
136
+ }
137
+ add_action( 'admin_head', __NAMESPACE__ . '\admin_header_revisions_styles' );
138
+
139
  /**
140
  * Add admin menu page.
141
  *
142
  * @return void
143
  */
144
  function admin_menu() {
145
+ add_options_page(
146
+ esc_html__( 'Ads.txt', 'ads-txt' ),
147
+ esc_html__( 'Ads.txt', 'ads-txt' ),
148
+ ADS_TXT_MANAGE_CAPABILITY,
149
+ 'adstxt-settings',
150
+ __NAMESPACE__ . '\settings_screen'
151
+ );
152
  }
153
  add_action( 'admin_menu', __NAMESPACE__ . '\admin_menu' );
154
 
158
  * @return void
159
  */
160
  function settings_screen() {
161
+ $post_id = get_option( ADS_TXT_MANAGER_POST_OPTION );
162
+ $post = false;
163
+ $content = false;
164
+ $errors = [];
165
+ $revision_count = 0;
166
+ $last_revision_id = false;
167
 
168
  if ( $post_id ) {
169
  $post = get_post( $post_id );
 
 
170
  }
171
+
172
+ if ( is_a( $post, 'WP_Post' ) ) {
173
+ $content = $post->post_content;
174
+ $revisions = wp_get_post_revisions( $post->ID );
175
+ $revision_count = count( $revisions );
176
+ $last_revision = array_shift( $revisions );
177
+ $last_revision_id = $last_revision ? $last_revision->ID : false;
178
+ $errors = get_post_meta( $post->ID, 'adstxt_errors', true );
179
+ $revisions_link = $last_revision_id ? admin_url( 'revision.php?adstxt=1&revision=' . $last_revision_id ) : false;
180
+
181
+ } else {
182
+
183
+ // Create an initial post so the second save creates a comparable revision.
184
+ $postarr = array(
185
+ 'post_title' => 'Ads.txt',
186
+ 'post_content' => '',
187
+ 'post_type' => 'adstxt',
188
+ 'post_status' => 'publish',
189
+ );
190
+
191
+ $post_id = wp_insert_post( $postarr );
192
+ if ( $post_id ) {
193
+ update_option( ADS_TXT_MANAGER_POST_OPTION, $post_id );
194
+ }
195
+ }
196
+ ?>
197
  <div class="wrap">
198
+ <div class="notice notice-error adstxt-notice existing-adstxt" style="display: none;">
199
+ <p><strong><?php echo esc_html_e( 'Existing Ads.txt file found', 'ads-txt' ); ?></strong></p>
200
+ <p><?php echo esc_html_e( 'An ads.txt file on the server will take precedence over any content entered here. You will need to rename or remove the existing ads.txt file before you will be able to see any changes you make on this screen.', 'ads-txt' ); ?></p>
201
+
202
+ <p><?php echo esc_html_e( 'Removed the existing file but are still seeing this warning?', 'ads-txt' ); ?> <a class="ads-txt-rerun-check" href="#"><?php echo esc_html_e( 'Re-run the check now', 'ads-txt' ); ?></a> <span class="spinner" style="float:none;margin:-2px 5px 0"></span></p>
203
+ </div>
204
+
205
+ <?php if ( ! empty( $errors ) ) : ?>
206
  <div class="notice notice-error adstxt-notice">
207
+ <p><strong><?php echo esc_html__( 'Your Ads.txt contains the following issues:', 'ads-txt' ); ?></strong></p>
208
  <ul>
209
  <?php
210
  foreach ( $errors as $error ) {
211
+ echo '<li>';
212
+
213
+ // Errors were originally stored as an array.
214
+ // This old style only needs to be accounted for here at runtime display.
215
+ if ( isset( $error['message'] ) ) {
216
+ $message = sprintf(
217
+ /* translators: Error message output. 1: Line number, 2: Error message */
218
+ __( 'Line %1$s: %2$s', 'ads-txt' ),
219
+ $error['line'],
220
+ $error['message']
221
+ );
222
+
223
+ echo esc_html( $message );
224
+ } else {
225
+ display_formatted_error( $error ); // WPCS: XSS ok.
226
+ }
227
+
228
+ echo '</li>';
229
  }
230
  ?>
231
  </ul>
232
  </div>
233
+ <?php endif; ?>
234
 
235
+ <h2><?php echo esc_html__( 'Manage Ads.txt', 'ads-txt' ); ?></h2>
236
 
237
  <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="adstxt-settings-form">
238
+ <input type="hidden" name="post_id" value="<?php echo esc_attr( $post_id ) ? esc_attr( $post_id ) : ''; ?>" />
239
  <input type="hidden" name="action" value="adstxt-save" />
240
  <?php wp_nonce_field( 'adstxt_save' ); ?>
241
 
242
+ <label class="screen-reader-text" for="adstxt_content"><?php echo esc_html__( 'Ads.txt content', 'ads-txt' ); ?></label>
243
  <textarea class="widefat code" rows="25" name="adstxt" id="adstxt_content"><?php echo esc_textarea( $content ); ?></textarea>
244
+ <?php
245
+ if ( $revision_count > 1 ) {
246
+ ?>
247
+ <div class="misc-pub-section misc-pub-revisions">
248
+ <?php
249
+ echo wp_kses_post(
250
+ sprintf(
251
+ /* translators: Post revisions heading. 1: The number of available revisions */
252
+ __( 'Revisions: <span class="adstxt-revision-count">%s</span>', 'ads-txt' ),
253
+ number_format_i18n( $revision_count )
254
+ )
255
+ );
256
+ ?>
257
+ <a class="hide-if-no-js" href="<?php echo esc_url( $revisions_link ); ?>">
258
+ <span aria-hidden="true">
259
+ <?php echo esc_html( __( 'Browse', 'ads-txt' ) ); ?>
260
+ </span> <span class="screen-reader-text">
261
+ <?php echo esc_html( __( 'Browse revisions', 'ads-txt' ) ); ?>
262
+ </span>
263
+ </a>
264
+ </div>
265
+ <?php
266
+ }
267
+ ?>
268
  <div id="adstxt-notification-area"></div>
269
 
270
  <p class="submit">
275
  </form>
276
 
277
  <script type="text/template" id="tmpl-adstext-notice">
 
 
 
 
 
 
278
  <# if ( ! _.isUndefined( data.errors ) ) { #>
279
  <div class="notice notice-error adstxt-notice adstxt-errors">
280
  <p><strong>{{ data.errors.error_message }}</strong></p>
281
  <# if ( ! _.isUndefined( data.errors.errors ) ) { #>
282
  <ul class="adstxt-errors-items">
283
  <# _.each( data.errors.errors, function( error ) { #>
284
+ <?php foreach ( array_keys( get_error_messages() ) as $error_type ) : ?>
285
+ <# if ( "<?php echo esc_html( $error_type ); ?>" === error.type ) { #>
286
+ <li>
287
+ <?php
288
+ display_formatted_error(
289
+ array(
290
+ 'line' => '{{error.line}}',
291
+ 'type' => $error_type,
292
+ 'value' => '{{error.value}}',
293
+ )
294
+ );
295
+ ?>
296
+ </li>
297
+ <# } #>
298
+ <?php endforeach; ?>
299
  <# } ); #>
300
  </ul>
301
  <# } #>
305
  <p class="adstxt-ays">
306
  <input id="adstxt-ays-checkbox" name="adstxt_ays" type="checkbox" value="y" />
307
  <label for="adstxt-ays-checkbox">
308
+ <?php esc_html_e( 'Update anyway, even though it may adversely affect your ads?', 'ads-txt' ); ?>
309
  </label>
310
  </p>
311
  <# } #>
314
  </script>
315
  </div>
316
 
317
+ <?php
318
  }
319
 
320
  /**
321
+ * Take an error array and output it as a message.
322
  *
323
  * @param array $error {
324
  * Array of error message components.
325
  *
 
326
  * @type int $line Line number of the error.
327
+ * @type string $type Type of error.
328
+ * @type string $value Optional. Value in question.
329
  * }
330
  *
331
+ * @return string|void
332
  */
333
+ function display_formatted_error( $error ) {
334
+ $messages = get_error_messages();
335
+
336
+ if ( ! isset( $messages[ $error['type'] ] ) ) {
337
+ return __( 'Unknown error', 'adstxt' );
338
+ }
339
+
340
+ if ( ! isset( $error['value'] ) ) {
341
+ $error['value'] = '';
342
+ }
343
+
344
+ $message = sprintf( esc_html( $messages[ $error['type'] ] ), '<code>' . esc_html( $error['value'] ) . '</code>' );
345
+
346
+ printf(
347
+ /* translators: Error message output. 1: Line number, 2: Error message */
348
+ esc_html__( 'Line %1$s: %2$s', 'ads-txt' ),
349
+ esc_html( $error['line'] ),
350
+ wp_kses_post( $message )
351
+ );
352
+ }
353
+
354
+ /**
355
+ * Get all non-generic error messages, translated and with placeholders intact.
356
+ *
357
+ * @return array Associative array of error messages.
358
+ */
359
+ function get_error_messages() {
360
+ $messages = array(
361
+ 'invalid_variable' => __( 'Unrecognized variable' ),
362
+ 'invalid_record' => __( 'Invalid record' ),
363
+ 'invalid_account_type' => __( 'Third field should be RESELLER or DIRECT' ),
364
+ /* translators: %s: Subdomain */
365
+ 'invalid_subdomain' => __( '%s does not appear to be a valid subdomain' ),
366
+ /* translators: %s: Exchange domain */
367
+ 'invalid_exchange' => __( '%s does not appear to be a valid exchange domain' ),
368
+ /* translators: %s: Alphanumeric TAG-ID */
369
+ 'invalid_tagid' => __( '%s does not appear to be a valid TAG-ID' ),
370
  );
371
 
372
+ return $messages;
373
+ }
374
+
375
+ /**
376
+ * Maybe display admin notices on the Ads.txt settings page.
377
+ *
378
+ * @return void
379
+ */
380
+ function admin_notices() {
381
+ if ( 'settings_page_adstxt-settings' !== get_current_screen()->base ) {
382
+ return;
383
+ }
384
+
385
+ if ( isset( $_GET['ads_txt_saved'] ) ) : // @codingStandardsIgnoreLine Nonce not required.
386
+ ?>
387
+ <div class="notice notice-success adstxt-notice adstxt-saved">
388
+ <p><?php echo esc_html__( 'Ads.txt saved', 'ads-txt' ); ?></p>
389
+ </div>
390
+ <?php
391
+ elseif ( isset( $_GET['revision'] ) ) : // @codingStandardsIgnoreLine Nonce not required.
392
+ ?>
393
+ <div class="notice notice-success adstxt-notice adstxt-saved">
394
+ <p><?php echo esc_html__( 'Revision restored', 'ads-txt' ); ?></p>
395
+ </div>
396
+ <?php
397
+ endif;
398
  }
399
+ add_action( 'admin_notices', __NAMESPACE__ . '\admin_notices' );
inc/post-type.php CHANGED
@@ -1,4 +1,9 @@
1
  <?php
 
 
 
 
 
2
 
3
  namespace Adstxt;
4
 
@@ -9,10 +14,11 @@ namespace Adstxt;
9
  */
10
  function register() {
11
  register_post_type(
12
- 'adstxt', array(
 
13
  'labels' => array(
14
- 'name' => _x( 'Ads.txt', 'post type general name', 'ads-txt' ),
15
- 'singular_name' => _x( 'Ads.txt', 'post type singular name', 'ads-txt' ),
16
  ),
17
  'public' => false,
18
  'hierarchical' => false,
@@ -32,7 +38,7 @@ function register() {
32
  'edit_post' => 'customize',
33
  'edit_posts' => 'customize',
34
  'edit_private_posts' => 'customize',
35
- 'edit_published_posts' => 'customize',
36
  'publish_posts' => 'customize',
37
  'read' => 'read',
38
  'read_post' => 'customize',
1
  <?php
2
+ /**
3
+ * Post Type functionality for Ads.txt.
4
+ *
5
+ * @package Ads_Txt_Manager
6
+ */
7
 
8
  namespace Adstxt;
9
 
14
  */
15
  function register() {
16
  register_post_type(
17
+ 'adstxt',
18
+ array(
19
  'labels' => array(
20
+ 'name' => esc_html_x( 'Ads.txt', 'post type general name', 'ads-txt' ),
21
+ 'singular_name' => esc_html_x( 'Ads.txt', 'post type singular name', 'ads-txt' ),
22
  ),
23
  'public' => false,
24
  'hierarchical' => false,
38
  'edit_post' => 'customize',
39
  'edit_posts' => 'customize',
40
  'edit_private_posts' => 'customize',
41
+ 'edit_published_posts' => 'edit_published_posts',
42
  'publish_posts' => 'customize',
43
  'read' => 'read',
44
  'read_post' => 'customize',
inc/save.php CHANGED
@@ -1,4 +1,9 @@
1
  <?php
 
 
 
 
 
2
 
3
  namespace Adstxt;
4
 
@@ -16,12 +21,14 @@ function save() {
16
  $_post = stripslashes_deep( $_POST );
17
  $doing_ajax = defined( 'DOING_AJAX' ) && DOING_AJAX;
18
 
19
- $post_id = $_post['post_id'];
20
  $ays = isset( $_post['adstxt_ays'] ) ? $_post['adstxt_ays'] : null;
21
 
22
  // Different browsers use different line endings.
23
  $lines = preg_split( '/\r\n|\r|\n/', $_post['adstxt'] );
24
- $sanitized = $errors = $response = array();
 
 
25
 
26
  foreach ( $lines as $i => $line ) {
27
  $line_number = $i + 1;
@@ -50,7 +57,6 @@ function save() {
50
  $post_id = wp_insert_post( $postarr );
51
 
52
  if ( $post_id ) {
53
- update_option( 'adstxt_post', $post_id );
54
  $response['saved'] = true;
55
  }
56
  }
@@ -59,15 +65,14 @@ function save() {
59
  $response['sanitized'] = $sanitized;
60
 
61
  if ( ! empty( $errors ) ) {
62
- // Transform errors into strings for easier i18n.
63
- $response['errors'] = array_map( __NAMESPACE__ . '\format_error', $errors );
64
  }
65
 
66
  echo wp_json_encode( $response );
67
  die();
68
  }
69
 
70
- wp_redirect( esc_url_raw( $_POST['_wp_http_referer'] ) . '&updated=true' );
71
  exit;
72
  }
73
  add_action( 'admin_post_adstxt-save', __NAMESPACE__ . '\save' );
@@ -96,9 +101,8 @@ function validate_line( $line, $line_number ) {
96
  // The spec currently supports CONTACT and SUBDOMAIN.
97
  if ( ! preg_match( '/^(CONTACT|SUBDOMAIN)=/i', $line ) ) {
98
  $errors[] = array(
99
- 'line' => $line_number,
100
- 'type' => 'warning',
101
- 'message' => __( 'Unrecognized variable', 'ads-txt' ),
102
  );
103
  } elseif ( 0 === stripos( $line, 'subdomain=' ) ) { // Subdomains should be, well, subdomains.
104
  // Disregard any comments.
@@ -111,14 +115,10 @@ function validate_line( $line, $line_number ) {
111
  // If there's anything other than one piece left something's not right.
112
  if ( 1 !== count( $subdomain ) || ! preg_match( $domain_regex, $subdomain[0] ) ) {
113
  $subdomain = implode( '', $subdomain );
114
- $errors[] = array(
115
- 'line' => $line_number,
116
- 'type' => 'warning',
117
- 'message' => sprintf(
118
- /* translators: %s: Subdomain */
119
- __( '"%s" does not appear to be a valid subdomain', 'ads-txt' ),
120
- esc_html( $subdomain )
121
- ),
122
  );
123
  }
124
  }
@@ -141,21 +141,16 @@ function validate_line( $line, $line_number ) {
141
 
142
  if ( ! preg_match( $domain_regex, $exchange ) ) {
143
  $errors[] = array(
144
- 'line' => $line_number,
145
- 'type' => 'warning',
146
- 'message' => sprintf(
147
- /* translators: %s: Exchange domain */
148
- __( '"%s" does not appear to be a valid exchange domain', 'ads-txt' ),
149
- esc_html( $exchange )
150
- ),
151
  );
152
  }
153
 
154
  if ( ! preg_match( '/^(RESELLER|DIRECT)$/i', $account_type ) ) {
155
  $errors[] = array(
156
- 'line' => $line_number,
157
- 'type' => 'error',
158
- 'message' => __( 'Third field should be RESELLER or DIRECT', 'ads-txt' ),
159
  );
160
  }
161
 
@@ -166,13 +161,9 @@ function validate_line( $line, $line_number ) {
166
  // TAG-IDs are meant to be checked against their DB - perhaps good for a service or the future.
167
  if ( ! empty( $tag_id ) && ! preg_match( '/^[a-f0-9]{16}$/', $tag_id ) ) {
168
  $errors[] = array(
169
- 'line' => $line_number,
170
- 'type' => 'warning',
171
- 'message' => sprintf(
172
- /* translators: %s: TAG-ID */
173
- __( '"%s" does not appear to be a valid TAG-ID', 'ads-txt' ),
174
- esc_html( $fields[3] )
175
- ),
176
  );
177
  }
178
  }
@@ -184,9 +175,8 @@ function validate_line( $line, $line_number ) {
184
  $sanitized = wp_strip_all_tags( $line );
185
 
186
  $errors[] = array(
187
- 'line' => $line_number,
188
- 'type' => 'error',
189
- 'message' => __( 'Invalid record', 'ads-txt' ),
190
  );
191
  }
192
 
@@ -198,3 +188,15 @@ function validate_line( $line, $line_number ) {
198
  'errors' => $errors,
199
  );
200
  }
 
 
 
 
 
 
 
 
 
 
 
 
1
  <?php
2
+ /**
3
+ * Save functionality for Ads.txt.
4
+ *
5
+ * @package Ads_Txt_Manager
6
+ */
7
 
8
  namespace Adstxt;
9
 
21
  $_post = stripslashes_deep( $_POST );
22
  $doing_ajax = defined( 'DOING_AJAX' ) && DOING_AJAX;
23
 
24
+ $post_id = (int) $_post['post_id'];
25
  $ays = isset( $_post['adstxt_ays'] ) ? $_post['adstxt_ays'] : null;
26
 
27
  // Different browsers use different line endings.
28
  $lines = preg_split( '/\r\n|\r|\n/', $_post['adstxt'] );
29
+ $sanitized = array();
30
+ $errors = array();
31
+ $response = array();
32
 
33
  foreach ( $lines as $i => $line ) {
34
  $line_number = $i + 1;
57
  $post_id = wp_insert_post( $postarr );
58
 
59
  if ( $post_id ) {
 
60
  $response['saved'] = true;
61
  }
62
  }
65
  $response['sanitized'] = $sanitized;
66
 
67
  if ( ! empty( $errors ) ) {
68
+ $response['errors'] = $errors;
 
69
  }
70
 
71
  echo wp_json_encode( $response );
72
  die();
73
  }
74
 
75
+ wp_safe_redirect( esc_url_raw( $_post['_wp_http_referer'] ) . '&updated=true' );
76
  exit;
77
  }
78
  add_action( 'admin_post_adstxt-save', __NAMESPACE__ . '\save' );
101
  // The spec currently supports CONTACT and SUBDOMAIN.
102
  if ( ! preg_match( '/^(CONTACT|SUBDOMAIN)=/i', $line ) ) {
103
  $errors[] = array(
104
+ 'line' => $line_number,
105
+ 'type' => 'invalid_variable',
 
106
  );
107
  } elseif ( 0 === stripos( $line, 'subdomain=' ) ) { // Subdomains should be, well, subdomains.
108
  // Disregard any comments.
115
  // If there's anything other than one piece left something's not right.
116
  if ( 1 !== count( $subdomain ) || ! preg_match( $domain_regex, $subdomain[0] ) ) {
117
  $subdomain = implode( '', $subdomain );
118
+ $errors[] = array(
119
+ 'line' => $line_number,
120
+ 'type' => 'invalid_subdomain',
121
+ 'value' => $subdomain,
 
 
 
 
122
  );
123
  }
124
  }
141
 
142
  if ( ! preg_match( $domain_regex, $exchange ) ) {
143
  $errors[] = array(
144
+ 'line' => $line_number,
145
+ 'type' => 'invalid_exchange',
146
+ 'value' => $exchange,
 
 
 
 
147
  );
148
  }
149
 
150
  if ( ! preg_match( '/^(RESELLER|DIRECT)$/i', $account_type ) ) {
151
  $errors[] = array(
152
+ 'line' => $line_number,
153
+ 'type' => 'invalid_account_type',
 
154
  );
155
  }
156
 
161
  // TAG-IDs are meant to be checked against their DB - perhaps good for a service or the future.
162
  if ( ! empty( $tag_id ) && ! preg_match( '/^[a-f0-9]{16}$/', $tag_id ) ) {
163
  $errors[] = array(
164
+ 'line' => $line_number,
165
+ 'type' => 'invalid_tagid',
166
+ 'value' => $fields[3],
 
 
 
 
167
  );
168
  }
169
  }
175
  $sanitized = wp_strip_all_tags( $line );
176
 
177
  $errors[] = array(
178
+ 'line' => $line_number,
179
+ 'type' => 'invalid_record',
 
180
  );
181
  }
182
 
188
  'errors' => $errors,
189
  );
190
  }
191
+
192
+ /**
193
+ * Delete `adstxt_errors` meta when restoring a revision.
194
+ *
195
+ * @param int $post_id Post ID, not revision ID.
196
+ *
197
+ * @return void
198
+ */
199
+ function clear_error_meta( $post_id ) {
200
+ delete_post_meta( $post_id, 'adstxt_errors' );
201
+ }
202
+ add_action( 'wp_restore_post_revision', __NAMESPACE__ . '\clear_error_meta', 10, 1 );
js/admin.js CHANGED
@@ -7,12 +7,38 @@
7
  mode: 'shell'
8
  } );
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  submit.on( 'click', function( e ){
11
  e.preventDefault();
12
 
13
  var textarea = $( document.getElementById( 'adstxt_content' ) ),
14
  notices = $( '.adstxt-notice' ),
15
  submit_wrap = $( 'p.submit' ),
 
16
  spinner = submit_wrap.find( '.spinner' );
17
 
18
  submit.attr( 'disabled', 'disabled' );
@@ -40,9 +66,7 @@
40
  }
41
 
42
  if ( 'undefined' !== typeof r.saved && r.saved ) {
43
- templateData.saved = {
44
- 'saved_message': adstxt.saved_message
45
- };
46
  } else {
47
  templateData.errors = {
48
  'error_message': adstxt.unknown_error
@@ -55,13 +79,20 @@
55
  'errors': r.errors
56
  }
57
  }
58
- notificationArea.html( notificationTemplate( templateData ) ).show();
 
 
 
 
 
 
 
59
  }
60
  })
61
  });
62
 
63
  $( '.wrap' ).on( 'click', '#adstxt-ays-checkbox', function( e ) {
64
- if ( true === $( this ).prop('checked') ) {
65
  submit.removeAttr( 'disabled' );
66
  } else {
67
  submit.attr( 'disabled', 'disabled' );
7
  mode: 'shell'
8
  } );
9
 
10
+ function checkForAdsFile( e ){
11
+ var currentTime = Date.now(),
12
+ adstxtUrl = '/ads.txt?currentTime=' + currentTime,
13
+ spinner = $( '.existing-adstxt .spinner' );
14
+
15
+ if ( false !== e ) {
16
+ e.preventDefault();
17
+ }
18
+
19
+ spinner.addClass( 'is-active' );
20
+
21
+ $.get( adstxtUrl, function( data, status ){
22
+ spinner.removeClass( 'is-active' );
23
+ $( '.existing-adstxt' ).show();
24
+ } ).fail( function() {
25
+ // Ads.txt not found
26
+ $( '.existing-adstxt' ).hide();
27
+ });
28
+ }
29
+
30
+ // Call our check when we first load the page
31
+ checkForAdsFile( false );
32
+
33
+ $( '.ads-txt-rerun-check' ).on( 'click', checkForAdsFile );
34
+
35
  submit.on( 'click', function( e ){
36
  e.preventDefault();
37
 
38
  var textarea = $( document.getElementById( 'adstxt_content' ) ),
39
  notices = $( '.adstxt-notice' ),
40
  submit_wrap = $( 'p.submit' ),
41
+ saveSuccess = false,
42
  spinner = submit_wrap.find( '.spinner' );
43
 
44
  submit.attr( 'disabled', 'disabled' );
66
  }
67
 
68
  if ( 'undefined' !== typeof r.saved && r.saved ) {
69
+ saveSuccess = true;
 
 
70
  } else {
71
  templateData.errors = {
72
  'error_message': adstxt.unknown_error
79
  'errors': r.errors
80
  }
81
  }
82
+
83
+ // Refresh after a successful save, otherwise show the error message.
84
+ if ( saveSuccess ) {
85
+ document.location = document.location + '&ads_txt_saved=1';
86
+ } else {
87
+ notificationArea.html( notificationTemplate( templateData ) ).show();
88
+ }
89
+
90
  }
91
  })
92
  });
93
 
94
  $( '.wrap' ).on( 'click', '#adstxt-ays-checkbox', function( e ) {
95
+ if ( true === $( this ).prop( 'checked' ) ) {
96
  submit.removeAttr( 'disabled' );
97
  } else {
98
  submit.attr( 'disabled', 'disabled' );
readme.txt CHANGED
@@ -1,14 +1,14 @@
1
  === Ads.txt Manager ===
2
  Contributors: 10up, helen, adamsilverstein, jakemgold
3
- Author URI: http://10up.com
4
  Plugin URI: https://github.com/10up/ads-txt
5
  Tags: ads.txt, ads, ad manager, advertising, publishing, publishers
6
  Requires at least: 4.9
7
- Tested up to: 4.9.1
8
  Requires PHP: 5.3
9
- Stable tag: trunk
10
  License: GPLv2 or later
11
- License URI: http://www.gnu.org/licenses/gpl-2.0.html
12
  Text Domain: ads-txt
13
 
14
  Create, manage, and validate your ads.txt from within WordPress, just like any other content asset. Requires PHP 5.3+ and WordPress 4.9+.
@@ -25,6 +25,7 @@ Ads.txt is an initiative by the Interactive Advertising Bureau to enable publish
25
 
26
  * Requires PHP 5.3+.
27
  * Requires WordPress 4.9+. Older versions of WordPress will not display any syntax highlighting and may break JavaScript and/or be unable to localize the plugin.
 
28
  * Rewrites need to be enabled. Without rewrites, WordPress cannot know to supply `/ads.txt` when requested.
29
  * Your site URL must not contain a path (e.g. `https://example.com/site/` or path-based multisite installs). While the plugin will appear to function in the admin, it will not display the contents at `https://example.com/site/ads.txt`. This is because the plugin follows the IAB spec, which requires that the ads.txt file be located at the root of a domain or subdomain.
30
 
@@ -32,6 +33,10 @@ Ads.txt is an initiative by the Interactive Advertising Bureau to enable publish
32
 
33
  We're closely monitoring continued developments in the ad fraud space, and see this plugin as not only a way to create and manage your ads.txt file but also be prepared for future changes and upgrades to specifications. Ads.cert is still in the extremely early stages so we don't see any immediate concerns with implementing ads.txt.
34
 
 
 
 
 
35
  == Screenshots ==
36
 
37
  1. Example of editing an ads.txt file with errors
@@ -42,7 +47,24 @@ We're closely monitoring continued developments in the ad fraud space, and see t
42
  3. Head to Settings → Ads.txt and add the records you need.
43
  4. Check it out at yoursite.com/ads.txt!
44
 
 
 
 
 
45
  == Changelog ==
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  = 1.0 =
48
  * Initial plugin release
1
  === Ads.txt Manager ===
2
  Contributors: 10up, helen, adamsilverstein, jakemgold
3
+ Author URI: https://10up.com
4
  Plugin URI: https://github.com/10up/ads-txt
5
  Tags: ads.txt, ads, ad manager, advertising, publishing, publishers
6
  Requires at least: 4.9
7
+ Tested up to: 5.3
8
  Requires PHP: 5.3
9
+ Stable tag: 1.2.0
10
  License: GPLv2 or later
11
+ License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
12
  Text Domain: ads-txt
13
 
14
  Create, manage, and validate your ads.txt from within WordPress, just like any other content asset. Requires PHP 5.3+ and WordPress 4.9+.
25
 
26
  * Requires PHP 5.3+.
27
  * Requires WordPress 4.9+. Older versions of WordPress will not display any syntax highlighting and may break JavaScript and/or be unable to localize the plugin.
28
+ * Ad blockers may break syntax highlighting and pre-save error checking on the edit screen.
29
  * Rewrites need to be enabled. Without rewrites, WordPress cannot know to supply `/ads.txt` when requested.
30
  * Your site URL must not contain a path (e.g. `https://example.com/site/` or path-based multisite installs). While the plugin will appear to function in the admin, it will not display the contents at `https://example.com/site/ads.txt`. This is because the plugin follows the IAB spec, which requires that the ads.txt file be located at the root of a domain or subdomain.
31
 
33
 
34
  We're closely monitoring continued developments in the ad fraud space, and see this plugin as not only a way to create and manage your ads.txt file but also be prepared for future changes and upgrades to specifications. Ads.cert is still in the extremely early stages so we don't see any immediate concerns with implementing ads.txt.
35
 
36
+ === What about app-ads.txt? ===
37
+
38
+ We plan on adding support for this file in the future! If you'd like to follow along with development or even help us get it done, please see [this GitHub issue](https://github.com/10up/ads-txt/issues/40).
39
+
40
  == Screenshots ==
41
 
42
  1. Example of editing an ads.txt file with errors
47
  3. Head to Settings → Ads.txt and add the records you need.
48
  4. Check it out at yoursite.com/ads.txt!
49
 
50
+ Note: If you already have an existing ads.txt file in the web root, the plugin will not read in the contents of that file, and changes you make in WordPress admin will not overwrite contents of the physical file.
51
+
52
+ You will need to rename or remove the existing ads.txt file (keeping a copy of the records it contains to put into the new settings screen) before you will be able to see any changes you make to ads.txt inside the WordPress admin.
53
+
54
  == Changelog ==
55
 
56
+ = 1.2.0 =
57
+ * **Added:** Make revisions accessible in the admin - now you can restore older versions of your ads.txt or view how it's changed over time (props [@adamsilverstein](https://github.com/adamsilverstein), [@helen](https://github.com/helen))
58
+ * **Added:** Show a notice on the edit screen if an ads.txt file exists on the server (props [@kkoppenhaver](https://github.com/kkoppenhaver), [@helen](https://github.com/helen), [@tomjn](https://github.com/tomjn), [@adamsilverstein](https://github.com/adamsilverstein))
59
+ * **Added:** Add a custom `edit_ads_txt` capability for granular assignment, which is assigned to administrators by default (props [@ethanclevenger91](https://github.com/ethanclevenger91), [@adamsilverstein](https://github.com/adamsilverstein))
60
+ * **Added:** Enable filtering of the output using `ads_txt_content` (props [@ethanclevenger91](https://github.com/ethanclevenger91))
61
+ * **Changed:** Updated documentation, automation, and coding standards (props [@jeffpaul](https://github.com/jeffpaul), [@adamsilverstein](https://github.com/adamsilverstein), [@helen](https://github.com/helen), [@mmcachran](https://github.com/mmcachran))
62
+ * **Fixed:** Early escaping (props [@tomjn](https://github.com/tomjn))
63
+ * **Fixed:** PHPCS issues and added PHPCS scanning (props [@adamsilverstein](https://github.com/adamsilverstein))
64
+
65
+ = 1.1 =
66
+ * Better error message formatting (wraps values in `<code>` tags for better readability)
67
+ * WordPress.com VIP-approved escaping
68
+
69
  = 1.0 =
70
  * Initial plugin release