: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
namespace TwitterFeed\Admin;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
class CTF_Notifications {
* Source of notifications content.
const SOURCE_URL = 'https://plugin.smashballoon.com/notifications.json';
const OPTION_NAME = 'ctf_notifications';
* JSON data contains notices for all plugins. This is used
* to select messages only meant for this plugin
const PLUGIN = 'twitter';
* Use this function to get the option name to allow
* inheritance for the New_User class
public function option_name() {
return self::OPTION_NAME;
* Use this function to get the source URL to allow
* inheritance for the New_User class
public function source_url() {
public function hooks() {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueues' ) );
add_action( 'ctf_admin_notices', array( $this, 'output' ) );
add_action( 'ctf_notification_update', array( $this, 'update' ) );
add_action( 'wp_ajax_ctf_dashboard_notification_dismiss', array( $this, 'dismiss' ) );
* Check if user has access and is enabled.
public function has_access() {
if ( current_user_can( 'manage_twitter_feed_options' ) || current_user_can( 'manage_options' ) ) {
return apply_filters( 'ctf_admin_notifications_has_access', $access );
* @param bool $cache Reference property cache if available.
public function get_option( $cache = true ) {
if ( $this->option && $cache ) {
$option = get_option( $this->option_name(), array() );
'update' => ! empty( $option['update'] ) ? $option['update'] : 0,
'events' => ! empty( $option['events'] ) ? $option['events'] : array(),
'feed' => ! empty( $option['feed'] ) ? $option['feed'] : array(),
'dismissed' => ! empty( $option['dismissed'] ) ? $option['dismissed'] : array(),
* Fetch notifications from feed.
public function fetch_feed() {
$res = wp_remote_get( $this->source_url() );
if ( is_wp_error( $res ) ) {
$body = wp_remote_retrieve_body( $res );
return $this->verify( json_decode( $body, true ) );
* Verify notification data before it is saved.
* @param array $notifications Array of notifications items to verify.
public function verify( $notifications ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
if ( ! is_array( $notifications ) || empty( $notifications ) ) {
$option = $this->get_option();
foreach ( $notifications as $notification ) {
// Ignore if not a targeted plugin
if ( ! empty( $notification['plugin'] ) && is_array( $notification['plugin'] ) && ! in_array( self::PLUGIN, $notification['plugin'], true ) ) {
// Ignore if max wp version detected
if ( ! empty( $notification['maxwpver'] ) && version_compare( get_bloginfo( 'version' ), $notification['maxwpver'], '>' ) ) {
// Ignore if max version has been reached
if ( ! empty( $notification['maxver'] ) && version_compare( $notification['maxver'], CTF_VERSION ) < 0 ) {
// Ignore if min version has not been reached
if ( ! empty( $notification['minver'] ) && version_compare( $notification['minver'], CTF_VERSION ) > 0 ) {
// Ignore if a specific ctf_status is empty or false
if ( ! empty( $notification['statuscheck'] ) ) {
$status_key = sanitize_key( $notification['statuscheck'] );
$ctf_statuses_option = get_option( 'ctf_statuses', array() );
if ( empty( $ctf_statuses_option[ $status_key ] ) ) {
// The message and license should never be empty, if they are, ignore.
if ( empty( $notification['content'] ) || empty( $notification['type'] ) ) {
// Ignore if license type does not match.
$license = ctf_is_pro_version() ? 'pro' : 'free';
if ( ! in_array( $license, $notification['type'], true ) ) {
if ( ! empty( $notification['end'] ) && ctf_get_current_time() > strtotime( $notification['end'] ) ) {
// Ignore if notification has already been dismissed.
if ( ! empty( $option['dismissed'] ) && in_array( $notification['id'], $option['dismissed'] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
if ( in_array( (int)$notification['id'], array( 9 ) ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
// TODO: Ignore if notification existed before installing CTF.
// Prevents bombarding the user with notifications after activation.
if ( ! empty( $activated )
&& ! empty( $notification['start'] )
&& $activated > strtotime( $notification['start'] ) ) {
* Verify saved notification data for active notifications.
* @param array $notifications Array of notifications items to verify.
public function verify_active( $notifications ) {
if ( ! is_array( $notifications ) || empty( $notifications ) ) {
// Remove notfications that are not active.
foreach ( $notifications as $key => $notification ) {
if ( ( ! empty( $notification['start'] ) && ctf_get_current_time() < strtotime( $notification['start'] ) )
|| ( ! empty( $notification['end'] ) && ctf_get_current_time() > strtotime( $notification['end'] ) ) ) {
unset( $notifications[ $key ] );
if ( empty( $notification['recent_install_override'] ) && $this->recently_installed() ) {
unset( $notifications[ $key ] );
// Ignore if max version has been reached
if ( ! empty( $notification['maxver'] ) && version_compare( $notification['maxver'], CTF_VERSION ) < 0 ) {
unset( $notifications[ $key ] );
// Ignore if max wp version detected
if ( ! empty( $notification['maxwpver'] ) && version_compare( get_bloginfo( 'version' ), $notification['maxwpver'], '>' ) ) {
unset( $notifications[ $key ] );
// Ignore if min version has not been reached
if ( ! empty( $notification['minver'] ) && version_compare( $notification['minver'], CTF_VERSION ) > 0 ) {
unset( $notifications[ $key ] );
// Ignore if a specific ctf_status is empty or false
if ( ! empty( $notification['statuscheck'] ) ) {
$status_key = sanitize_key( $notification['statuscheck'] );
$ctf_statuses_option = get_option( 'ctf_statuses', array() );
if ( empty( $ctf_statuses_option[ $status_key ] ) ) {
unset( $notifications[ $key ] );
public function recently_installed() {
$ctf_statuses_option = get_option( 'ctf_statuses', array() );
if ( ! isset( $ctf_statuses_option['first_install'] ) ) {
// Plugin was installed less than a week ago
if ( (int) $ctf_statuses_option['first_install'] > time() - WEEK_IN_SECONDS ) {
if ( ! $this->has_access() ) {
$option = $this->get_option();
// Update notifications using async task.
if ( empty( $option['update'] ) || ctf_get_current_time() > $option['update'] + DAY_IN_SECONDS ) {
$events = ! empty( $option['events'] ) ? $this->verify_active( $option['events'] ) : array();
$feed = ! empty( $option['feed'] ) ? $this->verify_active( $option['feed'] ) : array();
// If there is a new user notification, add it to the beginning of the notification list
$ctf_newuser = new CTF_New_User();
$newuser_notifications = $ctf_newuser->get();
if ( ! empty( $newuser_notifications ) ) {
$events = array_merge( $newuser_notifications, $events );
return array_merge( $events, $feed );
* Get notification count.
public function get_count() {
return count( $this->get() );
* Add a manual notification event.
* @param array $notification Notification data.
public function add( $notification ) {
if ( empty( $notification['id'] ) ) {
$option = $this->get_option();
if ( in_array( $notification['id'], $option['dismissed'] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
foreach ( $option['events'] as $item ) {
if ( $item['id'] === $notification['id'] ) {
$notification = $this->verify( array( $notification ) );
'update' => $option['update'],
'feed' => $option['feed'],
'events' => array_merge( $notification, $option['events'] ),
'dismissed' => $option['dismissed'],
* Update notification data from feed.
public function update() {
$feed = $this->fetch_feed();
$option = $this->get_option();
'update' => ctf_get_current_time(),
'events' => $option['events'],
'dismissed' => $option['dismissed'],
* Admin area Form Overview enqueues.
public function enqueues() {
if ( ! $this->has_access() ) {
$notifications = $this->get();
if ( empty( $notifications ) ) {
'ctf-admin-notifications',
CTF_PLUGIN_URL . "css/admin-notifications{$min}.css",
'ctf-admin-notifications',
CTF_PLUGIN_URL . "js/admin-notifications{$min}.js",
* Fields from the remote source contain placeholders to allow
* some messages to be used for multiple plugins.
* @param $notification array
public function replace_merge_fields( $content, $notification ) {
'{plugin}' => 'Twitter Feed',
'{amount}' => isset( $notification['amount'] ) ? $notification['amount'] : '',
'{platform}' => 'Twitter',
'{lowerplatform}' => 'twitter',
'{review-url}' => 'https://wordpress.org/support/plugin/custom-twitter-feeds/reviews/',
'{slug}' => 'custom-twitter-feeds',
'{campaign}' => 'twitter-free'
if ( ctf_is_pro_version() ) {
$merge_fields['{campaign}'] = 'twitter-pro';
$merge_fields['{plugin}'] = 'Twitter Feed Pro';
foreach ( $merge_fields as $find => $replace ) {
$content = str_replace( $find, $replace, $content );
* Output notifications on Twitter Feed admin area.
public function output() {
// if we are one single feed page then return
if ( isset( $_GET['feed_id'] ) ) {