Version Description
- Better error message formatting (wraps values in
<code>
tags for better readability) - WordPress.com VIP-approved escaping
Download this release
Release Info
Developer | helen |
Plugin | Ads.txt Manager |
Version | 1.1 |
Comparing to | |
See all releases |
Version 1.1
- ads-txt.php +39 -0
- inc/admin.php +250 -0
- inc/post-type.php +45 -0
- inc/save.php +186 -0
- js/admin.js +76 -0
- readme.txt +56 -0
ads-txt.php
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
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.1
|
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';
|
19 |
+
|
20 |
+
/**
|
21 |
+
* Display the contents of /ads.txt when requested.
|
22 |
+
*
|
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' );
|
inc/admin.php
ADDED
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
namespace AdsTxt;
|
4 |
+
|
5 |
+
/**
|
6 |
+
* Enqueue any necessary scripts.
|
7 |
+
*
|
8 |
+
* @param string $hook Hook name for the current screen.
|
9 |
+
*
|
10 |
+
* @return void
|
11 |
+
*/
|
12 |
+
function admin_enqueue_scripts( $hook ) {
|
13 |
+
if ( 'settings_page_adstxt-settings' !== $hook ) {
|
14 |
+
return;
|
15 |
+
}
|
16 |
+
|
17 |
+
wp_enqueue_script( 'adstxt', esc_url( 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' => esc_html__( 'Ads.txt saved', 'ads-txt' ),
|
22 |
+
'error_message' => esc_html__( 'Your Ads.txt contains the following issues:', 'ads-txt' ),
|
23 |
+
'unknown_error' => esc_html__( 'An unknown error occurred.', 'ads-txt' ),
|
24 |
+
);
|
25 |
+
|
26 |
+
wp_localize_script( 'adstxt', 'adstxt', $strings );
|
27 |
+
}
|
28 |
+
add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\admin_enqueue_scripts' );
|
29 |
+
|
30 |
+
/**
|
31 |
+
* Output some CSS directly in the head of the document.
|
32 |
+
*
|
33 |
+
* Should there ever be more than ~25 lines of CSS, this should become a separate file.
|
34 |
+
*
|
35 |
+
* @return void
|
36 |
+
*/
|
37 |
+
function admin_head_css() {
|
38 |
+
?>
|
39 |
+
<style>
|
40 |
+
.CodeMirror {
|
41 |
+
width: 100%;
|
42 |
+
min-height: 60vh;
|
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( esc_html__( 'Ads.txt', 'ads-txt' ), esc_html__( 'Ads.txt', 'ads-txt' ), 'manage_options', 'adstxt-settings', __NAMESPACE__ . '\settings_screen' );
|
59 |
+
}
|
60 |
+
add_action( 'admin_menu', __NAMESPACE__ . '\admin_menu' );
|
61 |
+
|
62 |
+
/**
|
63 |
+
* Output the settings screen.
|
64 |
+
*
|
65 |
+
* @return void
|
66 |
+
*/
|
67 |
+
function settings_screen() {
|
68 |
+
$post_id = get_option( 'adstxt_post' );
|
69 |
+
$post = false;
|
70 |
+
$content = false;
|
71 |
+
$errors = [];
|
72 |
+
|
73 |
+
if ( $post_id ) {
|
74 |
+
$post = get_post( $post_id );
|
75 |
+
}
|
76 |
+
|
77 |
+
if ( is_a( $post, 'WP_Post' ) ) {
|
78 |
+
$content = $post->post_content;
|
79 |
+
$errors = get_post_meta( $post->ID, 'adstxt_errors', true );
|
80 |
+
}
|
81 |
+
?>
|
82 |
+
<div class="wrap">
|
83 |
+
<?php if ( ! empty( $errors ) ) : ?>
|
84 |
+
<div class="notice notice-error adstxt-notice">
|
85 |
+
<p><strong><?php echo esc_html__( 'Your Ads.txt contains the following issues:', 'ads-txt' ); ?></strong></p>
|
86 |
+
<ul>
|
87 |
+
<?php
|
88 |
+
foreach ( $errors as $error ) {
|
89 |
+
echo '<li>';
|
90 |
+
|
91 |
+
// Errors were originally stored as an array
|
92 |
+
// This old style only needs to be accounted for here at runtime display
|
93 |
+
if ( isset( $error['message'] ) ) {
|
94 |
+
$message = sprintf(
|
95 |
+
/* translators: Error message output. 1: Line number, 2: Error message */
|
96 |
+
__( 'Line %1$s: %2$s', 'ads-txt' ),
|
97 |
+
$error['line'],
|
98 |
+
$error['message']
|
99 |
+
);
|
100 |
+
|
101 |
+
echo esc_html( $message );
|
102 |
+
} else {
|
103 |
+
/*
|
104 |
+
* Important: This is escaped piece-wise inside `format_error()`,
|
105 |
+
* as we cannot do absolute-end late escaping as normally recommended.
|
106 |
+
* This is because the placeholders in the translations can contain HTML,
|
107 |
+
* namely escaped data values wrapped in code tags.
|
108 |
+
* We don't have good JS translation tools yet and it's better to avoid duplication,
|
109 |
+
* so we use a single PHP function for both the JS template and in PHP.
|
110 |
+
*/
|
111 |
+
echo format_error( $error ); // WPCS: XSS ok.
|
112 |
+
}
|
113 |
+
|
114 |
+
echo '</li>';
|
115 |
+
}
|
116 |
+
?>
|
117 |
+
</ul>
|
118 |
+
</div>
|
119 |
+
<?php endif; ?>
|
120 |
+
|
121 |
+
<h2><?php echo esc_html__( 'Manage Ads.txt', 'ads-txt' ); ?></h2>
|
122 |
+
|
123 |
+
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="adstxt-settings-form">
|
124 |
+
<input type="hidden" name="post_id" value="<?php echo ( is_a( $post, 'WP_Post' ) ? esc_attr( $post->ID ) : '' ); ?>" />
|
125 |
+
<input type="hidden" name="action" value="adstxt-save" />
|
126 |
+
<?php wp_nonce_field( 'adstxt_save' ); ?>
|
127 |
+
|
128 |
+
<label class="screen-reader-text" for="adstxt_content"><?php echo esc_html__( 'Ads.txt content', 'ads-txt' ); ?></label>
|
129 |
+
<textarea class="widefat code" rows="25" name="adstxt" id="adstxt_content"><?php echo esc_textarea( $content ); ?></textarea>
|
130 |
+
|
131 |
+
<div id="adstxt-notification-area"></div>
|
132 |
+
|
133 |
+
<p class="submit">
|
134 |
+
<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php echo esc_attr( 'Save Changes' ); ?>">
|
135 |
+
<span class="spinner" style="float:none;vertical-align:top"></span>
|
136 |
+
</p>
|
137 |
+
|
138 |
+
</form>
|
139 |
+
|
140 |
+
<script type="text/template" id="tmpl-adstext-notice">
|
141 |
+
<# if ( ! _.isUndefined( data.saved ) ) { #>
|
142 |
+
<div class="notice notice-success adstxt-notice adstxt-saved">
|
143 |
+
<p>{{ data.saved.saved_message }}</p>
|
144 |
+
</div>
|
145 |
+
<# } #>
|
146 |
+
|
147 |
+
<# if ( ! _.isUndefined( data.errors ) ) { #>
|
148 |
+
<div class="notice notice-error adstxt-notice adstxt-errors">
|
149 |
+
<p><strong>{{ data.errors.error_message }}</strong></p>
|
150 |
+
<# if ( ! _.isUndefined( data.errors.errors ) ) { #>
|
151 |
+
<ul class="adstxt-errors-items">
|
152 |
+
<# _.each( data.errors.errors, function( error ) { #>
|
153 |
+
<?php foreach ( array_keys( get_error_messages() ) as $error_type ) : ?>
|
154 |
+
<# if ( "<?php echo esc_html( $error_type ); ?>" === error.type ) { #>
|
155 |
+
<li>
|
156 |
+
<?php
|
157 |
+
/*
|
158 |
+
* Important: This is escaped piece-wise inside `format_error()`,
|
159 |
+
* as we cannot do absolute-end late escaping as normally recommended.
|
160 |
+
* This is because the placeholders in the translations can contain HTML,
|
161 |
+
* namely escaped data values wrapped in code tags.
|
162 |
+
* We don't have good JS translation tools yet and it's better to avoid duplication,
|
163 |
+
* so we have to get them already-translated from PHP.
|
164 |
+
*/
|
165 |
+
echo format_error( array( // WPCS: XSS ok.
|
166 |
+
'line' => '{{error.line}}',
|
167 |
+
'type' => $error_type,
|
168 |
+
'value' => '{{error.value}}',
|
169 |
+
) );
|
170 |
+
?>
|
171 |
+
</li>
|
172 |
+
<# } #>
|
173 |
+
<?php endforeach; ?>
|
174 |
+
<# } ); #>
|
175 |
+
</ul>
|
176 |
+
<# } #>
|
177 |
+
</div>
|
178 |
+
|
179 |
+
<# if ( _.isUndefined( data.saved ) && ! _.isUndefined( data.errors.errors ) ) { #>
|
180 |
+
<p class="adstxt-ays">
|
181 |
+
<input id="adstxt-ays-checkbox" name="adstxt_ays" type="checkbox" value="y" />
|
182 |
+
<label for="adstxt-ays-checkbox">
|
183 |
+
<?php esc_html_e( 'Update anyway, even though it may adversely affect your ads?', 'ads-txt' ); ?>
|
184 |
+
</label>
|
185 |
+
</p>
|
186 |
+
<# } #>
|
187 |
+
|
188 |
+
<# } #>
|
189 |
+
</script>
|
190 |
+
</div>
|
191 |
+
|
192 |
+
<?php
|
193 |
+
}
|
194 |
+
|
195 |
+
/**
|
196 |
+
* Take an error array and turn it into a message.
|
197 |
+
*
|
198 |
+
* @param array $error {
|
199 |
+
* Array of error message components.
|
200 |
+
*
|
201 |
+
* @type int $line Line number of the error.
|
202 |
+
* @type string $type Type of error.
|
203 |
+
* @type string $value Optional. Value in question.
|
204 |
+
* }
|
205 |
+
*
|
206 |
+
* @return string Formatted error message.
|
207 |
+
*/
|
208 |
+
function format_error( $error ) {
|
209 |
+
$messages = get_error_messages();
|
210 |
+
|
211 |
+
if ( ! isset( $messages[ $error['type'] ] ) ) {
|
212 |
+
return __( 'Unknown error', 'adstxt' );
|
213 |
+
}
|
214 |
+
|
215 |
+
if ( ! isset( $error['value'] ) ) {
|
216 |
+
$error['value'] = '';
|
217 |
+
}
|
218 |
+
|
219 |
+
$message = sprintf( esc_html( $messages[ $error['type'] ] ), '<code>' . esc_html( $error['value'] ) . '</code>' );
|
220 |
+
|
221 |
+
$message = sprintf(
|
222 |
+
/* translators: Error message output. 1: Line number, 2: Error message */
|
223 |
+
__( 'Line %1$s: %2$s', 'ads-txt' ),
|
224 |
+
esc_html( $error['line'] ),
|
225 |
+
$message // This is escaped piece-wise above and may contain HTML (code tags) at this point
|
226 |
+
);
|
227 |
+
|
228 |
+
return $message;
|
229 |
+
}
|
230 |
+
|
231 |
+
/**
|
232 |
+
* Get all non-generic error messages, translated and with placeholders intact.
|
233 |
+
*
|
234 |
+
* @return array Associative array of error messages.
|
235 |
+
*/
|
236 |
+
function get_error_messages() {
|
237 |
+
$messages = array(
|
238 |
+
'invalid_variable' => __( 'Unrecognized variable' ),
|
239 |
+
'invalid_record' => __( 'Invalid record' ),
|
240 |
+
'invalid_account_type' => __( 'Third field should be RESELLER or DIRECT' ),
|
241 |
+
/* translators: %s: Subdomain */
|
242 |
+
'invalid_subdomain' => __( '%s does not appear to be a valid subdomain' ),
|
243 |
+
/* translators: %s: Exchange domain */
|
244 |
+
'invalid_exchange' => __( '%s does not appear to be a valid exchange domain' ),
|
245 |
+
/* translators: %s: Alphanumeric TAG-ID */
|
246 |
+
'invalid_tagid' => __( '%s does not appear to be a valid TAG-ID' ),
|
247 |
+
);
|
248 |
+
|
249 |
+
return $messages;
|
250 |
+
}
|
inc/post-type.php
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
namespace Adstxt;
|
4 |
+
|
5 |
+
/**
|
6 |
+
* Register the `adstxt` custom post type.
|
7 |
+
*
|
8 |
+
* @return void
|
9 |
+
*/
|
10 |
+
function register() {
|
11 |
+
register_post_type(
|
12 |
+
'adstxt', array(
|
13 |
+
'labels' => array(
|
14 |
+
'name' => esc_html_x( 'Ads.txt', 'post type general name', 'ads-txt' ),
|
15 |
+
'singular_name' => esc_html_x( 'Ads.txt', 'post type singular name', 'ads-txt' ),
|
16 |
+
),
|
17 |
+
'public' => false,
|
18 |
+
'hierarchical' => false,
|
19 |
+
'rewrite' => false,
|
20 |
+
'query_var' => false,
|
21 |
+
'delete_with_user' => false,
|
22 |
+
'supports' => array( 'revisions' ),
|
23 |
+
'map_meta_cap' => true,
|
24 |
+
'capabilities' => array(
|
25 |
+
'create_posts' => 'customize',
|
26 |
+
'delete_others_posts' => 'customize',
|
27 |
+
'delete_post' => 'customize',
|
28 |
+
'delete_posts' => 'customize',
|
29 |
+
'delete_private_posts' => 'customize',
|
30 |
+
'delete_published_posts' => 'customize',
|
31 |
+
'edit_others_posts' => 'customize',
|
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',
|
39 |
+
'read_private_posts' => 'customize',
|
40 |
+
),
|
41 |
+
)
|
42 |
+
);
|
43 |
+
}
|
44 |
+
|
45 |
+
add_action( 'init', __NAMESPACE__ . '\register' );
|
inc/save.php
ADDED
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
namespace Adstxt;
|
4 |
+
|
5 |
+
/**
|
6 |
+
* Process and save the ads.txt data.
|
7 |
+
*
|
8 |
+
* Handles both AJAX and POST saves via `admin-ajax.php` and `admin-post.php` respectively.
|
9 |
+
* AJAX calls output JSON; POST calls redirect back to the Ads.txt edit screen.
|
10 |
+
*
|
11 |
+
* @return void
|
12 |
+
*/
|
13 |
+
function save() {
|
14 |
+
current_user_can( 'customize' ) || die;
|
15 |
+
check_admin_referer( 'adstxt_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 = array();
|
25 |
+
$errors = array();
|
26 |
+
$response = array();
|
27 |
+
|
28 |
+
foreach ( $lines as $i => $line ) {
|
29 |
+
$line_number = $i + 1;
|
30 |
+
$result = validate_line( $line, $line_number );
|
31 |
+
|
32 |
+
$sanitized[] = $result['sanitized'];
|
33 |
+
if ( ! empty( $result['errors'] ) ) {
|
34 |
+
$errors = array_merge( $errors, $result['errors'] );
|
35 |
+
}
|
36 |
+
}
|
37 |
+
|
38 |
+
$sanitized = implode( PHP_EOL, $sanitized );
|
39 |
+
|
40 |
+
$postarr = array(
|
41 |
+
'ID' => $post_id,
|
42 |
+
'post_title' => 'Ads.txt',
|
43 |
+
'post_content' => $sanitized,
|
44 |
+
'post_type' => 'adstxt',
|
45 |
+
'post_status' => 'publish',
|
46 |
+
'meta_input' => array(
|
47 |
+
'adstxt_errors' => $errors,
|
48 |
+
),
|
49 |
+
);
|
50 |
+
|
51 |
+
if ( ! $doing_ajax || empty( $errors ) || 'y' === $ays ) {
|
52 |
+
$post_id = wp_insert_post( $postarr );
|
53 |
+
|
54 |
+
if ( $post_id ) {
|
55 |
+
update_option( 'adstxt_post', $post_id );
|
56 |
+
$response['saved'] = true;
|
57 |
+
}
|
58 |
+
}
|
59 |
+
|
60 |
+
if ( $doing_ajax ) {
|
61 |
+
$response['sanitized'] = $sanitized;
|
62 |
+
|
63 |
+
if ( ! empty( $errors ) ) {
|
64 |
+
$response['errors'] = $errors;
|
65 |
+
}
|
66 |
+
|
67 |
+
echo wp_json_encode( $response );
|
68 |
+
die();
|
69 |
+
}
|
70 |
+
|
71 |
+
wp_safe_redirect( esc_url_raw( $_post['_wp_http_referer'] ) . '&updated=true' );
|
72 |
+
exit;
|
73 |
+
}
|
74 |
+
add_action( 'admin_post_adstxt-save', __NAMESPACE__ . '\save' );
|
75 |
+
add_action( 'wp_ajax_adstxt-save', __NAMESPACE__ . '\save' );
|
76 |
+
|
77 |
+
/**
|
78 |
+
* Validate a single line.
|
79 |
+
*
|
80 |
+
* @param string $line The line to validate.
|
81 |
+
* @param string $line_number The line number being evaluated.
|
82 |
+
*
|
83 |
+
* @return array {
|
84 |
+
* @type string $sanitized Sanitized version of the original line.
|
85 |
+
* @type array $errors Array of errors associated with the line.
|
86 |
+
* }
|
87 |
+
*/
|
88 |
+
function validate_line( $line, $line_number ) {
|
89 |
+
$domain_regex = '/^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/i';
|
90 |
+
$errors = array();
|
91 |
+
|
92 |
+
if ( empty( $line ) ) {
|
93 |
+
$sanitized = '';
|
94 |
+
} elseif ( 0 === strpos( $line, '#' ) ) { // This is a full-line comment.
|
95 |
+
$sanitized = wp_strip_all_tags( $line );
|
96 |
+
} elseif ( 1 < strpos( $line, '=' ) ) { // This is a variable declaration.
|
97 |
+
// The spec currently supports CONTACT and SUBDOMAIN.
|
98 |
+
if ( ! preg_match( '/^(CONTACT|SUBDOMAIN)=/i', $line ) ) {
|
99 |
+
$errors[] = array(
|
100 |
+
'line' => $line_number,
|
101 |
+
'type' => 'invalid_variable',
|
102 |
+
);
|
103 |
+
} elseif ( 0 === stripos( $line, 'subdomain=' ) ) { // Subdomains should be, well, subdomains.
|
104 |
+
// Disregard any comments.
|
105 |
+
$subdomain = explode( '#', $line );
|
106 |
+
$subdomain = $subdomain[0];
|
107 |
+
|
108 |
+
$subdomain = explode( '=', $subdomain );
|
109 |
+
array_shift( $subdomain );
|
110 |
+
|
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' => 'invalid_subdomain',
|
117 |
+
'value' => $subdomain,
|
118 |
+
);
|
119 |
+
}
|
120 |
+
}
|
121 |
+
|
122 |
+
$sanitized = wp_strip_all_tags( $line );
|
123 |
+
|
124 |
+
unset( $subdomain );
|
125 |
+
} else { // Data records: the most common.
|
126 |
+
// Disregard any comments.
|
127 |
+
$record = explode( '#', $line );
|
128 |
+
$record = $record[0];
|
129 |
+
|
130 |
+
// Record format: example.exchange.com,pub-id123456789,RESELLER|DIRECT,tagidhash123(optional).
|
131 |
+
$fields = explode( ',', $record );
|
132 |
+
|
133 |
+
if ( 3 <= count( $fields ) ) {
|
134 |
+
$exchange = trim( $fields[0] );
|
135 |
+
$pub_id = trim( $fields[1] );
|
136 |
+
$account_type = trim( $fields[2] );
|
137 |
+
|
138 |
+
if ( ! preg_match( $domain_regex, $exchange ) ) {
|
139 |
+
$errors[] = array(
|
140 |
+
'line' => $line_number,
|
141 |
+
'type' => 'invalid_exchange',
|
142 |
+
'value' => $exchange,
|
143 |
+
);
|
144 |
+
}
|
145 |
+
|
146 |
+
if ( ! preg_match( '/^(RESELLER|DIRECT)$/i', $account_type ) ) {
|
147 |
+
$errors[] = array(
|
148 |
+
'line' => $line_number,
|
149 |
+
'type' => 'invalid_account_type',
|
150 |
+
);
|
151 |
+
}
|
152 |
+
|
153 |
+
if ( isset( $fields[3] ) ) {
|
154 |
+
$tag_id = trim( $fields[3] );
|
155 |
+
|
156 |
+
// TAG-IDs appear to be 16 character hashes.
|
157 |
+
// TAG-IDs are meant to be checked against their DB - perhaps good for a service or the future.
|
158 |
+
if ( ! empty( $tag_id ) && ! preg_match( '/^[a-f0-9]{16}$/', $tag_id ) ) {
|
159 |
+
$errors[] = array(
|
160 |
+
'line' => $line_number,
|
161 |
+
'type' => 'invalid_tagid',
|
162 |
+
'value' => $fields[3],
|
163 |
+
);
|
164 |
+
}
|
165 |
+
}
|
166 |
+
|
167 |
+
$sanitized = wp_strip_all_tags( $line );
|
168 |
+
} else {
|
169 |
+
// Not a comment, variable declaration, or data record; therefore, invalid.
|
170 |
+
// Early on we commented the line out for safety but it's kind of a weird thing to do with a JS AYS.
|
171 |
+
$sanitized = wp_strip_all_tags( $line );
|
172 |
+
|
173 |
+
$errors[] = array(
|
174 |
+
'line' => $line_number,
|
175 |
+
'type' => 'invalid_record',
|
176 |
+
);
|
177 |
+
}
|
178 |
+
|
179 |
+
unset( $record, $fields );
|
180 |
+
}
|
181 |
+
|
182 |
+
return array(
|
183 |
+
'sanitized' => $sanitized,
|
184 |
+
'errors' => $errors,
|
185 |
+
);
|
186 |
+
}
|
js/admin.js
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
( function( $, _ ) {
|
2 |
+
var submit = $( document.getElementById( 'submit' ) ),
|
3 |
+
notificationArea = $( document.getElementById( 'adstxt-notification-area' ) ),
|
4 |
+
notificationTemplate = wp.template( 'adstext-notice' ),
|
5 |
+
editor = wp.CodeMirror.fromTextArea( document.getElementById( 'adstxt_content' ), {
|
6 |
+
lineNumbers: true,
|
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' );
|
19 |
+
spinner.addClass( 'is-active' );
|
20 |
+
|
21 |
+
// clear any existing messages
|
22 |
+
notificationArea.hide();
|
23 |
+
notices.remove();
|
24 |
+
|
25 |
+
// Copy the code mirror contents into form for submission.
|
26 |
+
textarea.val( editor.getValue() );
|
27 |
+
|
28 |
+
$.ajax({
|
29 |
+
type: 'POST',
|
30 |
+
dataType: 'json',
|
31 |
+
url: ajaxurl,
|
32 |
+
data: $( '.adstxt-settings-form' ).serialize(),
|
33 |
+
success: function( r ) {
|
34 |
+
var templateData = {};
|
35 |
+
|
36 |
+
spinner.removeClass( 'is-active' );
|
37 |
+
|
38 |
+
if ( 'undefined' !== typeof r.sanitized ) {
|
39 |
+
textarea.val( r.sanitized );
|
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
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
if ( 'undefined' !== typeof r.errors && r.errors.length > 0 ) {
|
53 |
+
templateData.errors = {
|
54 |
+
'error_message': adstxt.error_message,
|
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' );
|
68 |
+
}
|
69 |
+
} );
|
70 |
+
|
71 |
+
editor.on( 'change', function() {
|
72 |
+
$( '.adstxt-ays' ).remove();
|
73 |
+
submit.removeAttr( 'disabled' );
|
74 |
+
} );
|
75 |
+
|
76 |
+
} )( jQuery, _ );
|
readme.txt
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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+.
|
15 |
+
|
16 |
+
== Description ==
|
17 |
+
|
18 |
+
Create, manage, and validate your ads.txt from within WordPress, just like any other content asset. Requires PHP 5.3+ and WordPress 4.9+.
|
19 |
+
|
20 |
+
=== What is ads.txt? ===
|
21 |
+
|
22 |
+
Ads.txt is an initiative by the Interactive Advertising Bureau to enable publishers to take control over who can sell their ad inventory. Through our work at 10up with various publishers, we've created a way to manage and validate your ads.txt file from within WordPress, eliminating the need to upload a file. The validation baked into the plugin helps avoid malformed records, which can cause issues that end up cached for up to 24 hours and can lead to a drop in ad revenue.
|
23 |
+
|
24 |
+
=== Technical Notes ===
|
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 |
+
|
31 |
+
=== What about ads.cert? ===
|
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
|
38 |
+
|
39 |
+
== Installation ==
|
40 |
+
1. Install the plugin via the plugin installer, either by searching for it or uploading a .zip file.
|
41 |
+
2. Activate the plugin.
|
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 |
+
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.
|
46 |
+
|
47 |
+
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.
|
48 |
+
|
49 |
+
== Changelog ==
|
50 |
+
|
51 |
+
= 1.1 =
|
52 |
+
* Better error message formatting (wraps values in `<code>` tags for better readability)
|
53 |
+
* WordPress.com VIP-approved escaping
|
54 |
+
|
55 |
+
= 1.0 =
|
56 |
+
* Initial plugin release
|