Do you want to collect stats from all the more than forty thousand plugins from the WordPress.org Plugins Directory? I don’t mean to collect plugin stats for just one plugin. I mean to collect stats about all of the plugins in the directory.
There are so many different ways that you could filter or sort the plugin data. For example, you can get all plugins that were created in a certain year. Or, rank all plugins in order of most 5-star ratings. Or, get the 100 oldest plugins. I will not go into every example.
I will show you a generic way for you to collect information about all the plugins in the directory. Then, you can modify it to gather the stats that you desire. Although, as an example, I will gather a list of plugins that have not been updated after December 31, 2010.
Summary of The Method
We’ll schedule a WP Cron event that will grab the plugin data from the WordPress.org API, one request at a time. The cron event will run every three minutes. The full gathering of data from all 55,000+ plugins can take as little as 8.1 hours. It may also take many more hours if your site doesn’t get much traffic. (Remember, WP Cron events require that someone visit your site.)
With every request, we’ll take the information that we want, and add that information to the previous information that we have collected. The plugin data that you desire will be saved to an option. This option is saved with no autoload, so it will not be hogging your memory. At the end of all the requests, you’ll remove the cron event. Then you’ll be left with all the data in an option that you can use as you wish.
Why a WP Cron event?
I use a cron event because we have to make 227 requests to the WordPress.org API. As of this writing, there are 56559 plugins in the directory. It seems that when you call the WP.org API with query_plugins
, it allows a maximum of 250 results per page. So, 56559 plugins at 250 per page gives us 227 pages.
Let’s begin.
Step 1: Add Helper Functions
First, add some helper functions. The first three helper functions are required for my example. This first one is a generic function that can be used to call the WordPress.org API for either plugins or themes.
/** * Makes a call to the WordPress.org API, v1.0 * @param string $action Either query_themes, theme_information, query_plugins, plugin_information * @param array $api_params * @return object Only the body of the raw response as a PHP object. */ function isa_call_wp_api( $action, $api_params = array() ) { $api = 'plugins'; if ( 'query_themes' == $action || 'query_themes' == $action ) { $api = 'themes'; } $url = 'https://api.wordpress.org/' . $api . '/info/1.0/'; if ( $ssl = wp_http_supports( array( 'ssl' ) ) ) { $url = set_url_scheme( $url, 'https' ); } $args = (object) $api_params; $http_args = array( 'body' => array( 'action' => $action, 'timeout' => 15, 'request' => serialize( $args ) ) ); $request = wp_remote_post( $url, $http_args ); if ( is_wp_error( $request ) ) { error_log('wp_remote_post to the WP.org API gave an error = '); error_log( print_r($request, true)); return false; } return maybe_unserialize( wp_remote_retrieve_body( $request ) ); }
This next helper function will be used to filter out any duplicates from our collection.
/** * Remove duplicate objects from an array of objects based on a duplicate object property */ function isa_unique_multidim_array($array, $key) { $temp_array = array(); $i = 0; $key_array = array(); foreach($array as $val) { if (!in_array($val->$key, $key_array)) { $key_array[$i] = $val->$key; $temp_array[$i] = $val; } $i++; } return $temp_array; }
The next helper function is for filtering the plugins by the “last updated” date. This is because, for my example, I will grab only plugins that have not been updated since before December 31, 2010. This function will eliminate plugins that were updated after December 31, 2010.
/** * Callback to filter plugins by last updated date. * Eliminates plugins updated after 12/31/2010 */ function isa_filter_plugins_by_date( $v ) { $d = strtotime($v->last_updated); return (mktime(0,0,0,12,31,2010) > $d); }
If you don’t want to filter the plugins by the last updated date, you can modify the filter to grab plugins based on your own conditions. This next variation will filter the plugins by the added date. This example filter will eliminate plugins that were added after 12/31/2008, which is useful if you only want to get the oldest plugins.
/** * Callback to filter plugins by ADDED date. * Eliminates plugins added after 12/31/2008 */ function isa_filter_plugins_by_added_date( $v ) { $d = strtotime($v->added); return (mktime(0,0,0,12,31,2008) > $d); }
As another example, this next function will filter the plugins by the number of active installs. It will eliminate plugins that have less than 100,000 active installs.
/** * Callback to filter plugins by most active installs. * Eliminates plugins with less than 100K installs. */ function isa_filter_plugins_by_installs( $v ) { return ( (int) $v->active_installs > 99999 ); }
If instead you want to filter by the number of 5-star ratings, this next function will do that. It will eliminate plugins that have less than 500 5-star ratings.
/** * Callback to filter themes by most 5-star ratings. * Eliminates plugins with less than 500 5-star ratings. */ function isa_filter_by_five_star_ratings( $v ) { if ( empty( $v->ratings[5] ) ) { return false; } else { return ( (int) $v->ratings[5] > 500 ); } }
Step 2: Add The Main Function
Now that the helper functions have been added, we move to the main function (Code block 6). Before I show you the whole function, I want to point out some important lines in the function.
This is the function that will gather the plugins’ stats and save them to an option. This function will be called by a WP Cron event (which we’ll set up later). The Cron event will call this function every three minutes. When this function is called, it will make one request to the WordPress.org API to get one page consisting of 250 plugins. Then, it will take the plugin data that we want and save it to an option.
The first time the function is called, it will get the first page of plugins. Upon every consecutive call, it will get the next page in line until all 227 pages are gotten. Every new batch of data will be added to the previously stored data in the same option.
Keeping The Data Small
I want to keep the size of plugin data small because it will be saved to an option. In order to keep the size of the data small, I will collect only the data that I need. For the API request, I will disable the fields that I don’t need. Yes, even removing one field of data makes a dent in the size of bytes. We must make sure that we can fit all the data in one option.
Pay special attention to lines 40–51 and lines 69–82 (highlighted below). This is where I remove all the unnecessary data. You may want to modify these lines for your own needs.
For this example, I only want to get plugins that have not been updated since before December 31, 2010. So, I don’t need much of the extra plugin information. Notice on lines 40–51 that I’ve disabled much of the fields so that it won’t grab all that extra information.
I also added one field, active_installs
, just because I want that piece of information (line 51). If you don’t need the number of active installs, remove line 51.
In your own case, you may want to know how many 5-star ratings a plugin has. You will need the ratings
field. In this case, you would want to delete line 43 so that you don’t disable the ratings field.
Please note that the ratings
field is different from the rating
field. The rating
is the average rating. On the other hand, ratings
is an array of how many ratings the plugin has received for 1 star, 2 stars, 3 stars, 4 stars, and 5 stars.
I recommend leaving the sections
and description
fields disabled, as I have them (lines 40 and 41). This is because those are large bits of data, and we want to make sure that we can fit all the data in one option.
Why do I have lines 69–82 to remove more fields? Why not just set them to false
like in lines 40–51? I do it this way because the WordPress.org API returns those fields no matter what. So, this is how I remove them from our data array.
Filtering Your List of Plugins
Pay special attention to line 65. This is the line that eliminates all the plugins that I don’t want in my list. It only keeps plugins whose “last updated” date is before December 31, 2010. You may want to filter by a different method. For example, if you want to use Code block 4 to filter by active installs, then you must change lines 64–65 (in Code block 6) to this:
// Get only those more than 100K active installs $plugins_data = array_filter( $response->plugins, 'isa_filter_plugins_by_installs' );
Or, if you want to use Code block 5 to filter by 5-star ratings, then you must change lines 64–65 (in Code block 6) to this:
// Get only those more than 500 5-star ratings $plugins_data = array_filter( $response->plugins, 'isa_filter_by_five_star_ratings' );
Alternatively, you could not filter at all. You may want to keep all 55,000+ plugins. I have not tested keeping all 55,000+ plugins’ data objects in one option. I think it should be okay, as long as it stays within 4 Gigabytes. So, to keep data for all plugins without eliminating any one, just change lines 64–65 to this:
$plugins_data = $response->plugins;
Naming Your Option
I’ve already mentioned that the plugin data will be saved to an option. In this example, I’m using the plugin data to display a list of abandoned plugins. Therefore, I named the option abandoned_plugins
. You can edit this to name the option whatever you like. If you do rename the option, be sure to rename it everywhere in the code block: lines 5, 17, 96, 112, 117, and 119.
Finally, here is the main function.
/** * Calls 1 page of the 227 pages (250 per page) of plugins at WP.org API. * It calls the next page in line, based on the isa_cron_wp_api_completed_page option. * * Saves the data by adding to our abandoned_plugins option. * * This is called every 3 mintues. */ function isa_gather_plugin_stats_from_api() { $running_batch = array(); $completed_page = get_option('isa_cron_wp_api_completed_page'); if ( empty( $completed_page ) ) { // start with page 1 $page_now = 1; delete_option('abandoned_plugins');// make sure to start fresh } else { $completed_page = (int) $completed_page; // Check if we are done. // @todo 227 will need to change as more plugins are added to the directory. // Currently, 56559 total plugins at 250 per page = 227. if ( $completed_page >= 227 ) { // we are done. error_log('cron job for gathering plugin stats is complete.'); return; } $page_now = $completed_page + 1; } $api_params = array( 'per_page' => 250, 'page' => $page_now, 'fields' => array( 'sections' => false, 'description' => false, 'homepage' => false, 'ratings' => false, 'rating' => false, 'requires' => false, 'downloaded' => false, 'tags' => false, 'donate_link' => false, 'short_description' => false, 'requires_php' => false, 'active_installs' => true, ) ); unset( $plugins_data ); $response = isa_call_wp_api( 'query_plugins', $api_params ); if ( empty( $response->plugins ) ) { error_log('EMPTY response = '); error_log( print_r($response, true)); return; } // Get only those with last updated date before 12/31/2010 $plugins_data = array_filter( $response->plugins, 'isa_filter_plugins_by_date' ); // Remove extra stuff to shrink data foreach ($plugins_data as $obj_key => $obj) { unset( $plugins_data[ $obj_key ]->version); unset( $plugins_data[ $obj_key ]->versions); unset( $plugins_data[ $obj_key ]->compatibility); unset( $plugins_data[ $obj_key ]->author_profile); unset( $plugins_data[ $obj_key ]->support_threads); unset( $plugins_data[ $obj_key ]->support_threads_resolved); unset( $plugins_data[ $obj_key ]->screenshots); unset( $plugins_data[ $obj_key ]->download_link); unset( $plugins_data[ $obj_key ]->contributors); unset( $plugins_data[ $obj_key ]->added); unset( $plugins_data[ $obj_key ]->stable_tag); unset( $plugins_data[ $obj_key ]->num_ratings); unset( $plugins_data[ $obj_key ]->tested); // sanitize $plugins_data[ $obj_key ]->name = sanitize_text_field($plugins_data[ $obj_key ]->name); $plugins_data[ $obj_key ]->slug = sanitize_title($plugins_data[ $obj_key ]->slug); $plugins_data[ $obj_key ]->author = strip_tags($plugins_data[ $obj_key ]->author); $plugins_data[ $obj_key ]->author = remove_accents($plugins_data[ $obj_key ]->author); if ( empty( $plugins_data[ $obj_key ]->name ) ) {// if name is empty, use slug $plugins_data[ $obj_key ]->name = $plugins_data[ $obj_key ]->slug; } } // Add these oldies to the previously saved array ... $option = get_option('abandoned_plugins', array()); $running_batch = array_merge( $option, $plugins_data ); // Remove duplicates. (I had many duplicates come through here.) $running_batch = isa_unique_multidim_array($running_batch,'slug'); // Log count ... $batch_count = count( $plugins_data ); if ( empty( $plugins_data ) ) { $batch_count = 0; } $running_total = count( $running_batch ); if ( empty( $running_batch ) ) { $running_total = 0; } error_log("Page $page_now count = $batch_count"); error_log("running_total = $running_total"); // Save option with no autoload $save_option = update_option( 'abandoned_plugins', $running_batch, 'no' ); if ( $save_option ) { // Update the completed_page option update_option('isa_cron_wp_api_completed_page', $page_now); error_log("SUCCESS. abandoned_plugins was updated for page $page_now."); } else { error_log( "FAIL. abandoned_plugins was NOT UPDATED for page $page_now."); } }
Step 3: Schedule The WP Cron Event
Now that the main function is in place, it’s time to schedule the WP Cron event. I only add this cron stuff on a temporary basis. After I’ve collected all the data, I remove this cron code because I don’t want it making a call every three minutes, forever.
First, this code creates a 3-minute interval so that we may then schedule events to occur every three minutes. Then, it schedules the event that will call our main function (above) every three minutes. I surrounded this code block with big, annoying code comments as a reminder to remove this stuff after our data collection is complete.
/**************************************************** * * BEGIN Temporary cron job to collect plugin stats * ****************************************************/ // Create 3-minute interval function isa_add_every_three_minutes($schedules) { $schedules['every_three_minutes'] = array( 'interval' => 180, 'display' => 'Every 3 Minutes' ); return $schedules; } add_filter( 'cron_schedules', 'isa_add_every_three_minutes' ); // Schedule my 3-minute event if ( ! wp_next_scheduled( 'isa_three_minute_hook' ) ) { wp_schedule_event( time(), 'every_three_minutes', 'isa_three_minute_hook' ); } // Every 3 minutes, call WP.org API to gather plugin stats, until I'm done gathering. add_action('isa_three_minute_hook', 'isa_gather_plugins_stats_action'); function isa_gather_plugins_stats_action() { isa_gather_plugin_stats_from_api(); } /**************************************************** * * END Temporary cron job to collect plugin stats * ****************************************************/
Step 4: Kick Off The Data Collection
Visit your site once to kick off the WP Cron event. This will begin the data collection. You can check to confirm that this began successfully by checking your error_log
file. If you have WP_DEBUG_LOG
enabled, your error log will be at /wp-content/debug.log
. Otherwise, check the error_log
file in your site’s root, or at wp-admin/error_log
.
In the file, you should see this to let you know that the data collection began:
Page 1 count = ## running_total = ## SUCCESS. abandoned_plugins was updated for page 1
Step 5: Wait.
The cron event will call our main function, and so get one page of plugins, every three minutes. To complete the job of getting data for every single plugin, it has to get 227 pages of plugins.
(Currently, 56559 plugins at 250 per page gives us 227 pages.)
One page every three minutes means that we can get about 20 pages per hour. The entire collection can be completed in as little as 8.1 hours. However, this is only if your site gets regular traffic. Your site has to get a visit in order to kick off WP Cron events. So, for this collection to be completed, your site needs at least 227 visits, and these visits have to spaced out at least three minutes apart.
Check your error log whenever you want to check how many pages have been collected. You’ll see something like this:
Page 1 count = ## running_total = ## SUCCESS. abandoned_plugins was updated for page 1 Page 2 count = ## running_total = ## SUCCESS. abandoned_plugins was updated for page 2 Page 3 count = ## running_total = ## SUCCESS. abandoned_plugins was updated for page 3
How Will You Know When The Job is Complete?
After all 227 pages have been collected, the error log will show this message.
cron job for gathering plugin stats is complete.
Step 6: Clean Up Your Code To Keep Your Site Optimized
After the data collection is complete, I like to remove all the code that we’ve added up to this point. At the very least, remove Code block 7 in order to cancel the scheduled event. Why would you want an extra hook being fired every three minutes? Now, don’t worry if you can’t get to this step right away. Even though the cron hook is fired every three minutes, it no longer makes any requests to the WordPress.org API.
In addition to removing the cron code (Code block 7), you should run the following code once. This will delete the helper option that kept track of which pages were completed (of the 227). For me, it’s necessary to delete this option because I will run this same code again in 3 months in order to update my stats. This option must be blank in order for the main function to begin again, successfully.
Once you add the following code and visit your site once to run it, you can remove it.
/**************************************************** * * Run this only once * ****************************************************/ add_action( 'init', 'isa_cleanup_after_gathering_stats'); function isa_cleanup_after_gathering_stats() { delete_option('isa_cron_wp_api_completed_page'); }
Step 7: Display Your Plugin Stats
Depending on which stats you collected, you can display them in a myriad of ways. I will show you one example of how to display a table of stats using a simple shortcode.
Before the shortcode, you have to add a new helper function that will sort our list. This one will sort the plugins by the date of last update.
// Callback to sort plugins by date, ascending function isa_sort_plugins_by_date( $a, $b ) { $a_time = strtotime( $a->last_updated ); $b_time = strtotime( $b->last_updated ); return ( $a_time - $b_time ); }
As a variation of that, if you were instead displaying the oldest plugins, you may want to sort them in order of their added date. In that case, you would use this instead:
// Callback to sort plugins by Added date, ascending function isa_sort_plugins_by_added_date( $a, $b ) { $a_time = strtotime( $a->added ); $b_time = strtotime( $b->added ); return ( $a_time - $b_time ); }
Now, let’s discuss some important lines in the actual shortcode function (Code block 10, below).
- My table headings are on lines 23–26 of the shortcode function. My table will display four fields: the last updated date of the plugin, the plugin name, author, and the number of active installs. If your fields are different, you will have to modify these lines. The data for these fields, respectively, are on lines 34–37. If your fields are different, you will have to modify these, too.
- If you changed the name of the option that holds the plugin data (above, in the “Naming Your Option” section), you must change
abandoned_plugins
on lines 9 and 12, to the name of your option. - Pay special attention to line 18. This is where the list is sorted. Here, I have sorted in order of the date of last update, in ascending order. You can change this line to sort any way you wish. However, remember to add a sorting function if you need one (like Code block 9). For example, you may want to sort your list in alphabetical order by plugin name. So, you would change line 18 to this:
usort( $plugins_list, 'isa_sort_by_plugin_name' );
And then you would add this callback function (instead of Code block 9) to sort by plugin name:
// Callback to sort plugins alphabetically by name function isa_sort_by_plugin_name( $a, $b ) { return strcasecmp( $a->name, $b->name ); }
As another sorting example, you may want to sort by number of active installs. In this case, you would change line 18 to this:
usort( $plugins_list, 'isa_sort_by_installs' );
And then you would add this callback function to sort by active installs:
// Callback to sort plugins by number of active installs, descending function isa_sort_by_installs( $a, $b ) { return $b->active_installs - $a->active_installs; }
- Finally, on line 44, I named the shortcode
abandoned_plugins
because that’s what I’ll use this one for. You should rename this to reflect your own use case.
On line 9, you may be wondering why I am querying the database instead of using get_option
? I do it this way because I had saved the option with autoload
disabled. This means that the option is not readily available in memory.
Finally, here is the shortcode function.
/** * Shortcode that displays the oldest abandoned plugins on the WordPress.org repository * in order by the oldest Last Updated date. * It shows the plugin name, author, last updated date, and number of active installs. */ function shortcode_abandoned_plugins() { global $wpdb; $plugins_list = $wpdb->get_var("SELECT option_value FROM $wpdb->options WHERE option_name = 'abandoned_plugins'"); if ( empty( $plugins_list ) ) { error_log('abandoned_plugins option is empty.'); return; } $plugins_list = maybe_unserialize( $plugins_list ); usort( $plugins_list, 'isa_sort_plugins_by_date' ); $html = '<table class="my-query-plugins-data">' . '<thead>' . '<tr>' . '<th>Last Updated</th>' . '<th>Plugin</th>' . '<th>Author</th>' . '<th>Active Installs</th>' . '</tr>' . '</thead>' . '<tbody>'; foreach ( $plugins_list as $p ) { $installs = (int) $p->active_installs; $html .= '<tr>' . '<td data-label="Last Updated">' . esc_html( substr( $p->last_updated, 0, 10 ) ) . '</td>' . '<td data-label="Plugin">' . esc_html( $p->name ) . '</td>' . '<td data-label="Author">' . esc_html( $p->author ) . '</td>' . '<td data-label="Active Installs">' . $installs . '</td>' . '</tr>'; } $html .= '</tbody></table>'; return $html; } add_shortcode('abandoned_plugins', 'shortcode_abandoned_plugins');
After you add the shortcode function, you can use the shortcode like any other shortcode: add it to a page or post. On that page, you’ll be able to see your table of plugin stats.
That is all. I hope you enjoyed this detailed tutorial on how to harvest plugin stats from the WordPress.org Plugin Directory.
Please feel free to report any errors that your find on this page.
Advanced Users
The following PHP script will harvest data on all 55,000+ plugins in under 5 minutes (approximately).
This script is only for advanced users that know how to use it. It’s here for reference, and to save someone time from having to write this. I described the purpose of this script in the blue note at the top of this page. Please read that note before using this script.
A few points to take note of:
- My average execution time for this script is 4.2 minutes.
- This script needs the helper functions from the Step 1.
- Line 64 eliminates all plugins that haven’t been updated since before December 31, 2010. You can change this filter for your own needs. (See the Filtering Your List of Plugins section where this is discussed.)
- Lines 16–27 and 69–82 are where you can enable or disable fields. (See the Keeping The Data Small section where this is discussed.)
- The plugins data will be saved to an option named
my_query_plugins
.
<?php /** * Makes ~163 consecutive calls to the WordPress.org API for all 55000+ plugins (250 per page/request). * * My average execution time for this script is 4.2 Minutes */ function isa_gather_plugin_stats_at_once( $page_range = array() ) { $time_start = microtime(true); set_time_limit(0);// my average execution time for this script is 4.2 Minutes $running_batch = array(); $api_params = array( 'per_page' => 250, 'fields' => array( 'sections' => false, 'description' => false, 'homepage' => false, 'ratings' => false, 'rating' => false, 'requires' => false, 'downloaded' => false, 'tags' => false, 'donate_link' => false, 'short_description' => false, 'requires_php' => false, 'active_installs' => true, ) ); // Call once to see how many pages there are $response = isa_call_wp_api( 'query_plugins', $api_params ); if ( empty( $response->plugins ) ) { error_log('EMPTY response = '); error_log( print_r($response, true)); // restore time limit and get out set_time_limit(30); return; } $pages = (int) $response->info['pages']; if ( empty( $page_range ) ) { $page_range = array( 1, $pages ); } // loop to make 227 consecutive calls to the API for ( $i = $page_range[0]; $i <= $page_range[1]; $i++ ) { unset( $plugins_data ); $api_params['page'] = $i; $response = isa_call_wp_api( 'query_plugins', $api_params ); if ( empty( $response->plugins ) ) { error_log('EMPTY response = '); error_log( print_r($response, true)); // restore time limit and get out set_time_limit(30); return; } error_log("ok page $i:"); // Get only those with last updated date before 12/31/2010 $plugins_data = array_filter( $response->plugins, 'isa_filter_plugins_by_date' ); // Remove extra stuff to shrink data foreach ($plugins_data as $obj_key => $obj) { unset( $plugins_data[ $obj_key ]->version); unset( $plugins_data[ $obj_key ]->versions); unset( $plugins_data[ $obj_key ]->compatibility); unset( $plugins_data[ $obj_key ]->author_profile); unset( $plugins_data[ $obj_key ]->support_threads); unset( $plugins_data[ $obj_key ]->support_threads_resolved); unset( $plugins_data[ $obj_key ]->screenshots); unset( $plugins_data[ $obj_key ]->download_link); unset( $plugins_data[ $obj_key ]->contributors); unset( $plugins_data[ $obj_key ]->added); unset( $plugins_data[ $obj_key ]->stable_tag); unset( $plugins_data[ $obj_key ]->num_ratings); unset( $plugins_data[ $obj_key ]->tested); // sanitize $plugins_data[ $obj_key ]->name = sanitize_text_field($plugins_data[ $obj_key ]->name); $plugins_data[ $obj_key ]->slug = sanitize_title($plugins_data[ $obj_key ]->slug); $plugins_data[ $obj_key ]->author = strip_tags($plugins_data[ $obj_key ]->author); $plugins_data[ $obj_key ]->author = remove_accents($plugins_data[ $obj_key ]->author); if ( empty( $plugins_data[ $obj_key ]->name ) ) {// if name is empty, use slug $plugins_data[ $obj_key ]->name = $plugins_data[ $obj_key ]->slug; } } // Add these plugins to the running list... $running_batch = array_merge( $running_batch, $plugins_data ); } // Remove duplicates. (I had many duplicates come through here.) $running_batch = isa_unique_multidim_array($running_batch,'slug'); // Log count ... $batch_count = count( $running_batch ); if ( empty( $running_batch ) ) { $batch_count = 0; error_log('ERROR, $running_batch is empty.'); // restore time limit and get out set_time_limit(30); return; } error_log('$running_batch count:' . $batch_count); // Save option with no autoload $save_option = update_option( 'my_query_plugins', $running_batch, 'no' ); // Log if option was saved. if ( $save_option ) { error_log("SUCCESS! my_query_plugins was updated."); } else { error_log( "FAIL, my_query_plugins was NOT UPDATED."); } $time_end = microtime(true); //dividing with 60 will give the execution time in minutes otherwise seconds $execution_time = ( $time_end - $time_start ) / 60; error_log('Total Execution Time: ' . $execution_time . ' Mins'); set_time_limit(30);// restore time limit return $save_option; }
Questions and Comments are Welcome