: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
Plugin Name: Multisite Post Reader
Plugin URI: http://cellarweb.com/wordpress-plugins/
Description: Shows posts from all subsites on a multisite via a shortcode used on pages/posts. SuperAdmins get an edit link. Optional parameters can limit number of posts and post length. Can be used for public pages.
Author: Rick Hellewell / CellarWeb.com
Author URI: http://CellarWeb.com
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Copyright (c) 2016-2019 by Rick Hellewell and CellarWeb.com
email: rhellewell@gmail.com
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2, as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
// ----------------------------------------------------------------
// ----------------------------------------------------------------
define('MPR_VERSION', "3.02");
define('MPR_VERSION_DATE', "(22 Mar 2023)");
global $atts; // used for the shortcode parameters
if (!mpr_is_requirements_met()) {
add_action('admin_init', 'mpr_disable_plugin');
add_action('admin_notices', 'mpr_show_notice');
add_action('network_admin_init', 'mpr_disable_plugin');
add_action('network_admin_notices', 'mpr_show_notice');
// Add settings link on plugin page
function mpr_settings_link($links) {
$settings_link = '<a href="options-general.php?page=mpr_settings" title="Multisite Post Reader">Multisite Post Reader Info/Usage</a>';
array_unshift($links, $settings_link);
$plugin = plugin_basename(__FILE__);
add_filter("plugin_action_links_$plugin", 'mpr_settings_link');
// build the class for all of this
class mpr_Settings_Page {
public function __construct() {
add_action('admin_menu', array($this, 'mpr_add_plugin_page'));
public function mpr_add_plugin_page() {
// This page will be under "Settings"
add_options_page('Multisite Post Reader Info/Usage', 'Multisite Post Reader Info/Usage', 'manage_options', 'mpr_settings', array($this, 'mpr_create_admin_page'));
public function mpr_create_admin_page() {
$this->options = get_option('mpr_options');
<div align='center' class = 'mpr_header'>
<img src="<?php echo plugin_dir_url(__FILE__); ?>assets/banner-1000x200.jpg" width="95%" alt="" class='mpr_shadow'>
<p align='center'><b>Version:</b> <?php echo MPR_VERSION . " " . MPR_VERSION_DATE; ?></p>
<!--<div class = 'mpr_header'>
<h1 align="center" >Multisite Post Reader</h1>
<div class="mpr_options">
<div class='mpr_sidebar'>
<!-- not sure why this one is needed ... -->
// print the Section text
public function mpr_print_section_info() {
print '<h3><strong>Information about Multisite Post Reader from CellarWeb.com</strong></h3>';
// end of the class stuff
$my_settings_page = new mpr_Settings_Page();
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// display the top info part of the page
// ----------------------------------------------------------------------------
function mpr_info_top() {
<p><strong>Multisite Post Reader</strong> allows you to use a <strong>[mpr_display]</strong> shortcode on a page/post to display all posts from all sites in a multisite system. This allows you (as the network SuperAdmin or non-network Admin) to monitor all posts on your multi-site system. Although meant for multisite systems by creating a page/post on the 'master' site, it will also work on standalone sites. It can be used on public pages. No CSS styling is added, so the posts will display using your theme's styling. Shortcode could also be used in a widget, but you would want to use parameters to limit count and length of displayed posts.</p>
<p>Each post has a clickable title, and also shows the publish date of the post. A "Read more" link is provided for all posts excerpts. Super-admins will see an Edit icon, which opens a new window/tab to edit the post. Each grouping of posts has a clickable link to the subsite's home page.</p>
<h2>Shortcode Options</h2>
<p>There are shortcode options/parameters for (adjust the values for your needs):</p>
<ul style="list-style-type: disc; list-style-position: inside;padding-left:20px;">
<li><strong>items=10</strong> show only the last 10 items (default all items, option used will be shown above each sites' posts group).</li>
<li><strong>days=4</strong> show only the last 4 days (default all dates, option used will be shown above each sites' picture group).</li>
<li><b>beforedate=YYYYMMDD</b> will select posts before the date. Note the required formatting for the date value. <i>Overridden if you select the 'days' parameter.</i></li>
<li><b>afterdate=YYYYMMDD</b> will select posts after the date. Note the required formatting for the date value. <i>Overridden if you select the 'days' parameter.</i></li>
<li><strong>showdate</strong> Suppress showing the date stamp of the post. (Default shows the datestamp.)</li>
<li><strong>excerpt</strong> will show the post excerpt, using the current word count for excerpt. (default is the entire post). The previous <b>words</b> option now functions like <b>excerpt</b>.</li>
<li><strong>showall</strong> show all posts, including drafts, scheduled, private; (default is only published posts, option used will be shown above each sites' posts group). Each post will show it's current status after the post publish date. This allows you to see all types of posts, including scheduled posts.</li>
<li><strong>showsiteinfo</strong> do not show subsite info (id, path) (default is to show the site info)</li>
<li><strong>disableheading</strong> do not show the selection criteria heading (default is to show selection criteria)</li>
<li><strong>showempty</strong> do not show subsites with no results (default=yes, will show 'none found' if no results for that subsite). Note that if a subsite does not have results, the subsite info (ID, path) will not be shown.</li>
<li><strong>showdivider</strong> Show the horizontal rule between posts (default will not show horizontal rule).</li>
<li><strong>includesites=1,3</strong> show only site with site_id #1 and #3 (default is all sites). Numbers are the site ID number. Separate multiple site id numbers with a comma; do not include quote characters. </li>
<li><strong>excludesites=2,4</strong> do not show sites with site_id of #2 and #4 (default is show all sites). Separate multiple site id numbers with a comma; do not include quote characters.</li>
<ul style="list-style-type: disc; list-style-position: inside;padding-left:22px;">
<li>You can use both includesites/excludesites parameters. The includesites list is processed first, then the excludesites list is processed.</li>
<li><strong>category='one, two, three'</strong> Only include the categories 'one', 'two', and 'three'. These are category <strong>names</strong> (slugs), not category ID numbers (as names are more likely to be the same across multisites, but category ID number might be different). Default is all categories. Separate categories with a comma, but include the quote character around all the category names. Note that any 'children' of a category will also be included for a specified category.</li>
<li><strong>type='x,y,z'</strong> Only include the post types indicated. Example: if you have a custom post type called 'product', then use type='product'. Separate multiple custom post type names with commas, and surround the post type names with a quote. Default is to only show 'posts'. </li>
<li><strong>debug</strong> Debugging mode: shows the SQL query, plus the number of records found in the query. Will show all shortcode parameters at the top of the output page. Not normally used in production, but helpful when you get strange results.</li>
<li><b>showsql</b> Shows the SQL statement used to query the data. For development only.</li>
<p>The parameters can be combined, as in <strong>[mpr_display days=4 items=10]</strong>. The optional parameters will be shown above each site's group of posts unless you use the <strong>disableheading=yes</strong> parameter. If a parameter has a non-alphanumeric character other than a comma (like spaces), enclose the parameters in quotes, as in <b>category='red apples, blue sky'</b>.</p>
<p>The individual post content is wrapped in a <strong>mpr_content</strong> CSS class, so you can add your own CSS to style the post content element. </p>
<h2>CSS Classes Used</h2>
<p>CSS classes are used for the elements of the posts being output. You can use these class names in your theme's Additional CSS, or in your custom-written theme. The plugin does not add any inline CSS. The CSS rules have no elements added by the plugin; you get to style the elements in your theme.</p>
<ul style="list-style-type: disc; list-style-position: inside;padding-left:20px;">
<li><strong>mpr_content</strong>: used around the entire post, including header, content, etc.</li>
<li><strong>mpr_the_permalink</strong>: for the link to the post - the output of <strong>the_permalink()</strong>.</li>
<li><strong>mpr_post_date</strong>: the post date/time stamp. Output can be disabled with the <b>showdate</b> parameter (see above).</li>
<li><strong>mpr_the_title</strong>: the post title - the output of <strong>the_title()</strong>.</li>
<li><strong>mpr_get_the_content</strong>: the post text/content, including read-more - the output of <strong>get_the_content()</strong>.</li>
<p><strong>Tell us how the Multisite Post Reader plugin works for you - leave a <a href="https://wordpress.org/support/plugin/multisite-post-reader/reviews/" title="Multisite Post Reader Reviews" target="_blank" >review or rating</a> on our plugin page. <a href="https://wordpress.org/support/plugin/multisite-post-reader" title="Help or Questions" target="_blank">Get Help or Ask Questions here</a>.</strong></p>
// ----------------------------------------------------------------------------
//here's the closing bracket for the is_admin thing
// ----------------------------------------------------------------------------
// register/deregister/uninstall hooks
register_activation_hook(__FILE__, 'mpr_register');
register_deactivation_hook(__FILE__, 'mpr_deregister');
register_uninstall_hook(__FILE__, 'mpr_uninstall');
// register/deregister/uninstall options (even though there aren't options)
function mpr_register() {
function mpr_deregister() {
function mpr_uninstall() {
// ----------------------------------------------------------------------------
function mpr_shortcodes_init() {
add_shortcode('mpr_display', 'mpr_setup_sites');
// get some CSS loaded for the settings page
wp_register_style('MPR_namespace', plugins_url('/css/settings.css', __FILE__), array(), MPR_VERSION);
wp_enqueue_style('MPR_namespace'); // gets the above css file in the proper spot
add_action('init', 'mpr_shortcodes_init', 999);
// ----------------------------------------------------------------------------
// here's where we do the work!
// ----------------------------------------------------------------------------
function mpr_setup_sites($atts = array()) {
ob_start(); // to get the shortcode output in the middle of the content.
// see https://wordpress.stackexchange.com/questions/47062/short-code-output-too-early
// sanitize any parameters using the sanitize_text_field callback
if (!is_array($atts)) {$atts = array();} // just in case
$atts = array_map('sanitize_text_field', $atts);
$atts = array_map('strtolower', $atts);
// set defaults for all $atts
// look for attributes without values (see https://wordpress.stackexchange.com/a/123073/29416
// - sets the value of that attribute as true if the attribute exists without a value
foreach ($atts as $item => $value) {
$new_value = explode(",", $value);
} else { $new_value = $value;}
$atts[$item] = $new_value;
// now need to fix $atts[0] = option_name; this looks for attributes that are numbers
// see https://wordpress.stackexchange.com/a/123073/29416
foreach ($atts as $attribute => $value) {
// echo "2nd loop $attribute with value of " . implode("",$value) . " <br>";
if (is_int($attribute)) {
$atts[implode("", $value)] = true;
unset($atts[$attribute]); // gets rid of the numeric item
$heading = array(); // for text to display if disableheading false (default)
$atts['datebefore'] = (isset($atts['datebefore'])) ? explode(',', $atts['datebefore']) : array();
if (count($atts['datebefore'])) {$args['before'] = array(
$atts['datebefore'][0], // YYYY
$atts['datebefore'][1], // MM
$atts['datebefore'][2], // DD
$heading[] = "Items before " . $atts['datebefore'][0] . "/" . $atts['datebefore'][1] . "/" . $atts['datebefore'][2] . "(YYYY/MM/DD)";
$args['date_query'] = array(
'year' => $atts['datebefore'][0],
'month' => $atts['datebefore'][1],
'day' => $atts['datebefore'][2],
$atts['dateafter'] = (isset($atts['dateafter'])) ? explode(',', $atts['dateafter']) : array();
if (count($atts['dateafter'])) {$args['after'] = array(
$atts['dateafter'][0], // YYYY
$atts['dateafter'][1], // MM
$atts['dateafter'][2], // DD
$heading[] = "Items after " . $atts['dateafter'][0] . "/" . $atts['dateafter'][1] . "/" . $atts['dateafter'][2] . "(YYYY/MM/DD)";
$args['date_query'] = array(
'year' => $atts['dateafter'][0],
'month' => $atts['dateafter'][1],
'day' => $atts['dateafter'][2],
$atts['days'] = (isset($atts['days'])) ? $atts['days'] : 0;
// get current date - days
$newDate = date('Y-m-d', strtotime(' - ' . $atts['days'] . ' days'));
// computer 'days' ago from current date
// set that date string as the dateafter
$args['after'] = $newdate;
$heading[] = "Including sites " . implode(", ", $atts['includesites']);
if ((isset($atts['dateafter'])) OR (isset($atts['dateafter']))) {
unset($args['date_query']);
$heading[] = "(The 'days' option is overriding the 'datebefore' and 'dateafter' options.)";
// convert following to array output
$atts['includesites'] = (isset($atts['includesites'])) ? $atts['includesites'] : array();
if (count($atts['includesites']) > 0) {
$heading[] = "Including sites " . implode(", ", $atts['includesites']);
$atts['excludesites'] = (isset($atts['excludesites'])) ? explode(",", $atts['excludesites']) : array();
if (count($atts['excludesites']) > 0) {
$heading[] = "Including sites " . implode(", ", $atts['excludesites']);
$atts['category'] = (isset($atts['category'])) ? $atts['category'] : array();
if (count($atts['category'])) {
$args['category_name'] = implode(",", $atts['category']);
//$args['category_name'] = "red";
$atts['type'] = (isset($atts['type'])) ? explode(",", $atts['type']) : "post";
$atts['post_type'] = (isset($atts['post_type'])) ? explode(",", $atts['post_type']) : "any";
// set all the atts to default or settings value
$atts['items'] = (isset($atts['items'])) ? $atts['items'] : 0;
$atts['words'] = (isset($atts['words'])) ? $atts['words'] : 0;
$atts['excerpt'] = (isset($atts['excerpt'])) ? 1 : 0;
$atts['showall'] = (isset($atts['showall'])) ? 1 : 0;
$atts['showsiteinfo'] = (isset($atts['showsiteinfo'])) ? 1 : 0;
$atts['disableheading'] = (isset($atts['disableheading'])) ? 1 : 0;
$atts['showempty'] = (isset($atts['showempty'])) ? 1 : 0;
$atts['showdivider'] = (isset($atts['showdivider'])) ? 1 : 0;
$atts['showdate'] = (isset($atts['showdate'])) ? 1 : 0;
$atts['debug'] = (isset($atts['debug'])) ? 1 : 0;
$atts['category'] = (isset($atts['category'])) ? $atts['category'] : array();
// $atts['type'] = (isset($atts['type'])) ? $atts['type'] : 0;
$atts['tag'] = (isset($atts['tag'])) ? $atts['tag'] : 0;
$atts['nodate'] = (isset($atts['nodate'])) ? $atts['nodate'] : 0;
$atts['showsql'] = (isset($atts['showsql'])) ? 1 : 0; // new in version 2.50
if ($atts['debug'] == true) {
echo "<strong>Debug:</strong> Shortcode Attributes array<br>";
$args['post_type'] = $atts['type'];
mpr_get_sites_array($atts, $args, $heading); // get the sites array, and loop through them in that function
// this will flush the output, putting shortcode content where it belongs
// don't ob_flush/etc anywhere else
// --------------------------------------------------------------------------------
// ===============================================================================
// functions to display all posts
// ===============================================================================
Styles and code 'functionated' for displaying all posts files
adapted from http://alijafarian.com/responsive-image-grids-using-css/
// --------------------------------------------------------------------------------
- $atts = shortcode attributes
- $args = args for the wp_query object
- $heading = text to show is 'disableheading' is set false (default is true)
function mpr_get_sites_array($atts = array(), $args = array(), $heading = "") {
global $posts; // need to ensure post data available to any called functions in here
<div class='mpr_heading'>
if (!$atts['disableheading']) { // building and displaying heading text
if ($atts['showempty']) {
$atts['showempty'] = false;
// $heading[] = "Subsites without entries not shown";
$subsites_object = get_sites();
$subsites = mpr_objectToArray($subsites_object);
$subsites_copy = array();
// headings created and shown at top of all posts
if ($atts['includesites']) {
$heading[] = "Including subsites: " . implode(", ", $atts['includesites']);
foreach ($subsites as $subsite) {
$found_include = in_array($subsite['blog_id'], $atts['includesites']);
$subsites_copy[] = $subsite; // add site
$subsites = $subsites_copy;
if ($atts['excludesites']) {
$heading[] = "Excluding subsites: " . implode(", ", $atts['excludesites']);
foreach ($subsites as $subsite) {
$found_exclude = in_array($subsite['blog_id'], $atts['excludesites']);
$subsites_copy[] = $subsite; // add site
if (count($subsites_copy)) {
$subsites = $subsites_copy;
if (isset($atts['showempty'])) {
$heading[] = "Subsites without entries not shown";
if ($atts['items'] > 0) {
$x = (is_array($atts['items'])) ? implode(", ", $atts['items']) : "All ";
$heading[] = $x . " posts";
$heading[] = "Last " . $atts['days'] . " days";
if (($atts['words'] > 0) OR ($atts['excerpt'])) {
$heading[] = "Showing the excerpt.";
if (isset($atts['category'])) {
$heading[] = "Category: " . implode(", ", $atts['category']);
$daystring = $atts['days'] . " days ago";
/* if ($atts['showall']) {
if (!isset($args['post_type'])) {
$args['post_type'] = 'post';
$heading[] = " All posts";
$args['post_type'] = "any";
$heading[] = 'Post Type = ' . $atts['type'];
$args['post_type'] = $atts['type'];
/* not needed, previously set as default = post
if ((!count($atts['type'])) AND (!$args['post_type'])) {
$args['post_type'] = "post";
$heading[] = 'Tag(s) = ' . $atts['tag'];
if ($atts['showdivider']) {echo "<hr>";} else {echo "<br>";}
foreach ($heading as $item) {
echo " - " . $item . "<br>";
foreach ($subsites as $subsite) {
$subsite_id = $subsite['blog_id'];
$subsite_name = get_blog_details($subsite_id)->blogname;
$subsite_path = $subsite['path'];
$subsite_domain = $subsite['domain'];
switch_to_blog($subsite_id);
if ($atts['showsiteinfo']) {
$heading = "<hr>Site:<strong> $subsite_id - '$subsite_name'</strong> - <strong>";
$thesite_url = site_url();
$heading .= "<a href='" . $thesite_url . "' target='_blank'>" . $thesite_url . "</a>";
$heading .= "</strong><hr>";
$xsiteurl = "https://" . $subsite_domain . $subsite_path;
mpr_site_show_posts($xsiteurl, $atts, $heading, $args);
// don't ob_flush, that's taken care of elsewhere
if ($atts['showdivider']) { echo "<hr>";} // last entry, show divider if enabled
// --------------------------------------------------------------------------------
// list all posts on all multisite sites
// inspired by https://wisdmlabs.com/blog/how-to-list-posts-from-all-blogs-on-wordpress-multisite/
// --------------------------------------------------------------------------------
// display posts on all sites
function mpr_site_show_posts($xsiteurl = "", $atts = array(), $heading = array(), $args = array()) {
// ensure args are set with at least the post type parameter, otherwise, query results will be empty
if (!is_array($args)) {$args = array();$args['post_type'] = "post";}
// see https://developer.wordpress.org/reference/classes/wp_query/#tag-parameters
// - for syntax of query element and value type (string, comma string, array)
$query = new WP_Query($args);
$records_found = $query->post_count;
echo "<br><b>SQL Query:</b><br>" . $query->request . "<br>";
echo "<strong>Found:</strong> " . $records_found . " records<br>";
if (!$query->have_posts()) {
if (isset($atts['debug'])) {echo "<strong>Debug: </strong> (none found)<br>";}
if (isset($atts['showempty'])) {
if ($query->have_posts()) {
while ($query->have_posts()) {
if ($atts['showdivider']) {echo "<hr>";} else {echo "<br>";}
<div class="mpr_content"> <strong> <a href="<?php the_permalink();?>" title="<?php the_title_attribute();?>" class="mpr_the_permalink">
<div class="mpr_the_title"><?php the_title();?></div>
</a></strong>