';
- $output .= '
' . esc_html( $item['post_id'] ) . '
';
- // Build the fields for the conditionals
- $output .= '
';
- // Build the fields for the normal columns
- $output .= '
';
- $output .= '
' . __( 'URL Variables', 'ad-code-manager' ) . '
';
- foreach ( (array) $item['url_vars'] as $slug => $value ) {
- $output .= '
';
- $column_id = 'acm-column[' . $slug . ']';
- $output .= '';
- // Support for select dropdowns
- $ad_code_args = wp_filter_object_list( $ad_code_manager->current_provider->ad_code_args, array( 'key' => $slug ) );
- $ad_code_arg = array_shift( $ad_code_args );
- if ( isset( $ad_code_arg['type'] ) && 'select' == $ad_code_arg['type'] ) {
- $output .= '';
- } else {
- $output .= '';
- }
- $output .= '
';
- }
- $output .= '
';
- // Build the field for the priority
- $output .= '
';
- $output .= '
' . __( 'Priority', 'ad-code-manager' ) . '
';
- $output .= '';
- $output .= '';
- // Build the field for the logical operator
- $output .= '
';
- $output .= '
' . __( 'Logical Operator', 'ad-code-manager' ) . '
';
- $output .= '';
- $output .= '';
-
- $output .= '
';
- return $output;
- }
/**
*
@@ -301,8 +279,18 @@ function column_conditionals( $item ) {
*/
function row_actions_output( $item ) {
$output = '';
- // $row_actions['preview-ad-code'] = '' . esc_html( $validation_result->get_error_message() ) . '
' );
+ }
+ // Store the error message in a transient for display after redirect.
+ set_transient( 'acm_validation_error_' . get_current_user_id(), $validation_result->get_error_message(), 30 );
+ $message = 'validation-error';
+ break;
+ }
+
if ( 'add' === $method ) {
$id = $this->create_ad_code( $ad_code_vals );
} else {
@@ -353,14 +451,26 @@ function handle_admin_action() {
* Get the ad codes stored in our custom post type
*/
function get_ad_codes( $query_args = array() ) {
+ /**
+ * Filters the allowed query parameters for retrieving ad codes.
+ *
+ * Controls which query parameters can be passed to modify the ad codes query.
+ *
+ * @since 0.1
+ *
+ * @param array $allowed_params Array of allowed query parameter names. Default array( 'offset' ).
+ */
$allowed_query_params = apply_filters( 'acm_allowed_get_posts_args', array( 'offset' ) );
-
/**
- * Configuration filter: acm_ad_code_count
+ * Filters the maximum number of ad codes to retrieve.
+ *
+ * By default, queries are limited to 50 ad codes.
+ * Use this filter to increase or decrease the limit.
+ *
+ * @since 0.1
*
- * By default we limit query to 50 ad codes
- * Use this filter to change limit
+ * @param int $count Maximum number of ad codes to retrieve. Default 50.
*/
$args = array(
'post_type' => $this->post_type,
@@ -617,9 +727,38 @@ function action_load_ad_code_manager() {
* Print the admin interface for managing the ad codes.
*/
function admin_view_controller() {
+ // Check for edit action.
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in render_edit_page.
+ if ( isset( $_GET['action'] ) && 'edit' === $_GET['action'] && isset( $_GET['id'] ) ) {
+ $this->render_edit_page();
+ return;
+ }
+
require_once dirname( AD_CODE_MANAGER_FILE ) . '/views/ad-code-manager.tpl.php';
}
+ /**
+ * Render the dedicated edit page for an ad code.
+ *
+ * @since 0.10.0
+ */
+ function render_edit_page() {
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a view, nonce verified on form submission.
+ $ad_code_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
+ $ad_code = $this->get_ad_code( $ad_code_id );
+
+ // Handle invalid ID gracefully.
+ if ( ! $ad_code ) {
+ wp_die(
+ esc_html__( 'Invalid ad code ID.', 'ad-code-manager' ),
+ esc_html__( 'Error', 'ad-code-manager' ),
+ array( 'back_link' => true )
+ );
+ }
+
+ require_once dirname( AD_CODE_MANAGER_FILE ) . '/views/edit-ad-code.tpl.php';
+ }
+
/**
* Register a custom widget to display ad zones
*/
@@ -639,7 +778,6 @@ function register_scripts_and_styles() {
}
wp_enqueue_style( 'acm-style', plugins_url( '/', AD_CODE_MANAGER_FILE ) . '/acm.css', array(), AD_CODE_MANAGER_VERSION );
- wp_enqueue_script( 'acm', plugins_url( '/', AD_CODE_MANAGER_FILE ) . '/acm.js', array( 'jquery' ), AD_CODE_MANAGER_VERSION, true );
}
/**
@@ -722,12 +860,6 @@ function register_ad_codes( $ad_codes = array() ) {
continue;
}
- /**
- * Configuration filter: acm_default_url
- * If you don't specify a URL for your ad code when registering it in
- * the WordPress admin or at a code level, you can simply apply it with
- * a custom filter defined.
- */
$ad_code['priority'] = $ad_code['priority'] === '' ? 10 : (int) $ad_code['priority']; // make sure priority is int, if it's unset, we set it to 10
// Make sure our operator is 'OR' or 'AND'
@@ -735,6 +867,16 @@ function register_ad_codes( $ad_codes = array() ) {
$operator = $this->logical_operator;
}
+ /**
+ * Filters the default URL for an ad code.
+ *
+ * If you don't specify a URL for your ad code when registering it in
+ * the WordPress admin or at a code level, you can apply it with this filter.
+ *
+ * @since 0.1
+ *
+ * @param string $url The ad code URL. May be empty.
+ */
$this->register_ad_code( $default_tag['tag'], apply_filters( 'acm_default_url', $ad_code['url'] ), $ad_code['conditionals'], array_merge( $default_tag['url_vars'], $ad_code['url_vars'] ), $ad_code['priority'], $ad_code['operator'] );
}
}
@@ -750,10 +892,13 @@ function register_ad_codes( $ad_codes = array() ) {
*/
function get_acm_tag( $tag_id ): string {
/**
- * See http://adcodemanager.wordpress.com/2013/04/10/hi-all-on-a-dotcom-site-that-uses/
+ * Filters whether to disable ad rendering.
+ *
+ * By default, ads are disabled on post previews to prevent ad tracking issues.
*
- * Configuration filter: acm_disable_ad_rendering
- * Should be boolean, defaulting to disabling ads on previews
+ * @since 0.1
+ *
+ * @param bool $disable Whether to disable ad rendering. Default is the result of is_preview().
*/
if ( apply_filters( 'acm_disable_ad_rendering', is_preview() ) ) {
return '';
@@ -772,9 +917,15 @@ function get_acm_tag( $tag_id ): string {
}
/**
- * Configuration filter: acm_output_html
- * Support multiple ad formats ( e.g. Javascript ad tags, or simple HTML tags )
+ * Filters the output HTML template for an ad tag.
+ *
+ * Support multiple ad formats (e.g., JavaScript ad tags, or simple HTML tags)
* by adjusting the HTML rendered for a given ad tag.
+ *
+ * @since 0.1
+ *
+ * @param string $output_html The HTML template with tokens (e.g., '%url%', '%width%').
+ * @param string $tag_id The ad tag ID being rendered.
*/
$output_html = apply_filters( 'acm_output_html', $this->current_provider->output_html, $tag_id );
@@ -782,9 +933,17 @@ function get_acm_tag( $tag_id ): string {
$output_html = str_replace( '%url%', $code_to_display['url'], $output_html );
/**
- * Configuration filter: acm_output_tokens
- * Register output tokens depending on the needs of your setup. Tokens are the
- * keys to be replaced in your script URL.
+ * Filters the output tokens for an ad tag.
+ *
+ * Tokens are placeholders in the output HTML that get replaced with actual values.
+ * Register custom tokens depending on your setup.
+ *
+ * @since 0.1
+ *
+ * @param array $output_tokens Associative array of tokens and their replacement values.
+ * Keys should include % (e.g., '%width%' => '300').
+ * @param string $tag_id The ad tag ID being rendered.
+ * @param array $code_to_display The matching ad code configuration.
*/
$output_tokens = apply_filters( 'acm_output_tokens', $this->current_provider->output_tokens, $tag_id, $code_to_display );
foreach ( (array) $output_tokens as $token => $val ) {
@@ -792,11 +951,42 @@ function get_acm_tag( $tag_id ): string {
}
/**
- * Configuration filter: acm_output_html_after_tokens_processed
- * In some rare cases you might want to filter html after the tokens are processed
+ * Filters the output HTML after token replacement.
+ *
+ * Use this filter for final modifications to the ad HTML after all tokens
+ * have been processed. Useful for adding wrappers or final adjustments.
+ *
+ * @since 0.2
+ *
+ * @param string $output_html The processed HTML with all tokens replaced.
+ * @param string $tag_id The ad tag ID being rendered.
*/
$output_html = apply_filters( 'acm_output_html_after_tokens_processed', $output_html, $tag_id );
+ /**
+ * Configuration filter: acm_wrapper_classes
+ * Filter the CSS classes applied to the ad wrapper div.
+ *
+ * @since 0.8.0
+ *
+ * @param array $classes Array of CSS class names. Default: 'acm-wrapper', 'acm-tag-{tag_id}'.
+ * @param string $tag_id The ad tag ID being rendered.
+ */
+ $wrapper_classes = apply_filters(
+ 'acm_wrapper_classes',
+ array( 'acm-wrapper', 'acm-tag-' . sanitize_html_class( $tag_id ) ),
+ $tag_id
+ );
+
+ // Allow disabling the wrapper by returning an empty array.
+ if ( ! empty( $wrapper_classes ) && is_array( $wrapper_classes ) ) {
+ $output_html = sprintf(
+ '', $output, 'Widget should output wrapper when filter returns true.' );
+ $this->assertStringContainsString( '
', $output, 'Widget should output closing wrapper when filter returns true.' );
+ }
+
+ /**
+ * Test widget outputs title when filter allows empty content.
+ *
+ * @covers ACM_Ad_Zones::widget
+ */
+ public function test_widget_outputs_title_when_filter_allows_empty_content(): void {
+ add_filter( 'acm_display_empty_widget', '__return_true' );
+
+ $args = array(
+ 'before_widget' => '', $output, 'Widget should output wrapper with valid ad code.' );
+ $this->assertStringContainsString( '
', $output, 'Widget should output closing wrapper.' );
+ }
+
+ /**
+ * Test that acm_tag action is still fired (backwards compatibility).
+ *
+ * @covers ACM_Ad_Zones::widget
+ */
+ public function test_acm_tag_action_is_fired(): void {
+ $action_fired = false;
+ $action_tag = null;
+
+ $action_callback = function ( $tag_id ) use ( &$action_fired, &$action_tag ) {
+ $action_fired = true;
+ $action_tag = $tag_id;
+ };
+
+ add_action( 'acm_tag', $action_callback, 5 ); // Priority 5 to run before the default handler.
+
+ $args = array(
+ 'before_widget' => '' . esc_html( $message_text ) . '
';
+ echo '