Commit b6fb99b5 by François C.

Mis à jour pour refléter le code du module en production sur le WP de La Graine

parent 7e528490
Useful links about plugin dev have been initialy put as comment at the end of wosmpl.php file.
Here they are:
https://developer.wordpress.org/plugins/security/checking-user-capabilities/
https://developer.wordpress.org/plugins/security/data-validation/
https://developer.wordpress.org/plugins/security/securing-input/
https://developer.wordpress.org/plugins/security/securing-output/
https://developer.wordpress.org/plugins/security/nonces/
https://codex.wordpress.org/Plugin_API/Filter_Reference
https://codex.wordpress.org/Plugin_API/Action_Reference
https://developer.wordpress.org/plugins/hooks/advanced-topics/
https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/
https://developer.wordpress.org/plugins/shortcodes/basic-shortcodes/
https://developer.wordpress.org/plugins/shortcodes/shortcodes-with-parameters
https://developer.wordpress.org/plugins/settings/custom-settings-page/
\ No newline at end of file
=== Plugin Name ===
Contributors: (this should be a list of wordpress.org userid's)
Donate link: http://example.com/
Tags: comments, spam
Requires at least: 3.0.1
Tested up to: 3.4
Stable tag: 4.3
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Here is a short description of the plugin. This should be no more than 150 characters. No markup here.
== Description ==
This is the long description. No limit, and you can use Markdown (as well as in the following sections).
For backwards compatibility, if this section is missing, the full length of the short description will be used, and
Markdown parsed.
A few notes about the sections above:
* "Contributors" is a comma separated list of wp.org/wp-plugins.org usernames
* "Tags" is a comma separated list of tags that apply to the plugin
* "Requires at least" is the lowest version that the plugin will work on
* "Tested up to" is the highest version that you've *successfully used to test the plugin*. Note that it might work on
higher versions... this is just the highest one you've verified.
* Stable tag should indicate the Subversion "tag" of the latest stable version, or "trunk," if you use `/trunk/` for
stable.
Note that the `readme.txt` of the stable tag is the one that is considered the defining one for the plugin, so
if the `/trunk/readme.txt` file says that the stable tag is `4.3`, then it is `/tags/4.3/readme.txt` that'll be used
for displaying information about the plugin. In this situation, the only thing considered from the trunk `readme.txt`
is the stable tag pointer. Thus, if you develop in trunk, you can update the trunk `readme.txt` to reflect changes in
your in-development version, without having that information incorrectly disclosed about the current stable version
that lacks those changes -- as long as the trunk's `readme.txt` points to the correct stable tag.
If no stable tag is provided, it is assumed that trunk is stable, but you should specify "trunk" if that's where
you put the stable version, in order to eliminate any doubt.
== Installation ==
This section describes how to install the plugin and get it working.
e.g.
1. Upload `plugin-name.php` to the `/wp-content/plugins/` directory
1. Activate the plugin through the 'Plugins' menu in WordPress
1. Place `<?php do_action('plugin_name_hook'); ?>` in your templates
== Frequently Asked Questions ==
= A question that someone might have =
An answer to that question.
= What about foo bar? =
Answer to foo bar dilemma.
== Screenshots ==
1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif). Note that the screenshot is taken from
the /assets directory or the directory that contains the stable readme.txt (tags or trunk). Screenshots in the /assets
directory take precedence. For example, `/assets/screenshot-1.png` would win over `/tags/4.3/screenshot-1.png`
(or jpg, jpeg, gif).
2. This is the second screen shot
== Changelog ==
= 1.0 =
* A change since the previous version.
* Another change.
= 0.5 =
* List versions from most recent at top to oldest at bottom.
== Upgrade Notice ==
= 1.0 =
Upgrade notices describe the reason a user should upgrade. No more than 300 characters.
= 0.5 =
This version fixes a security related bug. Upgrade immediately.
== Arbitrary section ==
You may provide arbitrary sections, in the same format as the ones above. This may be of use for extremely complicated
plugins where more information needs to be conveyed that doesn't fit into the categories of "description" or
"installation." Arbitrary sections will be shown below the built-in sections outlined above.
== A brief Markdown Example ==
Ordered list:
1. Some feature
1. Another feature
1. Something else about the plugin
Unordered list:
* something
* something else
* third thing
Here's a link to [WordPress](http://wordpress.org/ "Your favorite software") and one to [Markdown's Syntax Documentation][markdown syntax].
Titles are optional, naturally.
[markdown syntax]: http://daringfireball.net/projects/markdown/syntax
"Markdown is what the parser uses to process much of the readme file"
Markdown uses email style notation for blockquotes and I've been told:
> Asterisks for *emphasis*. Double it up for **strong**.
`<?php code(); // goes in backticks ?>`
\ No newline at end of file
<?php
add_action( 'wp_ajax_wosmpl_upload_file', 'wosmpl_wosmpl_upload_file' );
add_action( 'wp_ajax_wosmpl_upload_file', 'wosmpl_ajax_upload_file' );
function wosmpl_reduce_image($file_path,$ext,$width,$height,$new_widths) {
foreach ($new_widths as $new_width) {
$new_height = $height * ($new_width/$width);
if ($ext == 'jpeg') {
$source = imagecreatefromjpeg($file_path);
} else if ($ext == 'png') {
$source = imagecreatefrompng($file_path);
}
$thumb = imagecreatetruecolor($new_width, $new_height);
imagecopyresampled($thumb, $source, 0, 0, 0, 0, $new_width,$new_height, $width, $height);
$reduced_fn = str_replace('.','-'.$new_width.'.',$file_path);
if ($ext == 'jpeg') {
imagejpeg($thumb,$reduced_fn);
} else if ($ext == 'png') {
imagepng($thumb,$reduced_fn);
}
}
}
function wosmpl_wosmpl_upload_file() {
function wosmpl_ajax_upload_file()
{
check_ajax_referer( 'wosmpl_ajax', '_ajax_nonce' );
$return = [];
$file = $_POST['file'] ;
if ($file) {
$base64 = sanitize_text_field($file['base64']);
$base64 = str_replace('data:'.$file['type'].';base64,', '', $base64);
list($mtype,$ext) = explode('/', $file['type']);
$fname = hash('sha1',$file['base64']) . '.' . $ext;
$file_path = get_option('wosmpl_upload_dir') .'/' .$fname;
if (file_put_contents($file_path,base64_decode($base64))) {
$return['file_url'] = str_replace(get_home_path(),'/',$file_path);
if (strpos($mtype, 'image') !== FALSE) {
$return['img_info'] = getimagesize($file_path);
$width = $return['img_info'][0];
$height = $return['img_info'][1];
wosmpl_reduce_image($file_path,$ext,$width,$height,[125,350]);
}
} else {
$return['error'] = 'File cannot be saved !';
}
wp_send_json(
wosmpl_upload_file($_POST['file'])
);
}
wp_send_json($return);
wp_die(); // all ajax handlers should die when finished
}
\ No newline at end of file
}
......@@ -24,6 +24,7 @@ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/metaboxes.php';
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/options.php';
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/settings.php';
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/ajax.php';
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/image.php';
class WosmPL_Admin {
......@@ -158,6 +159,10 @@ class WosmPL_Admin {
* @since 1.0.0
*/
public function wosmpl_save_post($post_id){
if (empty($_POST)) {
return;
}
$errors = [];
//TODO use of wp_verify_nonce
$wosmpl_latlon = $_POST['wosmpl_latlon'];
......@@ -169,11 +174,11 @@ class WosmPL_Admin {
$wosmpl_partner_specialty = $_POST['wosmpl_partner_specialty'];
$wosmpl_partner_challenge = $_POST['wosmpl_partner_challenge'];
$wosmpl_partner_phone = $_POST['wosmpl_partner_phone'];
$wosmpl_partner_phone_visible = $_POST['wosmpl_partner_phone_visible'];
$wosmpl_partner_phone_visible = $_POST['wosmpl_partner_phone_visible'] ?? '';
$wosmpl_partner_email = $_POST['wosmpl_partner_email'];
$wosmpl_partner_email_visible = $_POST['wosmpl_partner_email_visible'];
$wosmpl_partner_email_visible = $_POST['wosmpl_partner_email_visible'] ?? '';
$wosmpl_partner_website = $_POST['wosmpl_partner_website'];
$wosmpl_partner_website_visible = $_POST['wosmpl_partner_website_visible'];
$wosmpl_partner_website_visible = $_POST['wosmpl_partner_website_visible'] ?? '';
//Data verif
if ($wosmpl_geo_cat && !is_numeric($wosmpl_geo_cat)) {
$errors[] = 'Invalid category type';
......@@ -269,6 +274,7 @@ class WosmPL_Admin {
'rewrite' => array('slug' => 'partenaires'),
'query_var' => true,
'menu_position'=> 5,
'has_archive' => TRUE,
//'menu_icon' => 'dashicons-video-alt',
'supports' => array(
'title',
......@@ -312,4 +318,4 @@ class WosmPL_Admin_Error_Message {
printf( '<script type="text/javascript">alert("%s")</script>', $this->_message );
}
}
\ No newline at end of file
}
<?php
function wosmpl_reduce_image($file_path,$ext,$width,$height,$new_widths) {
foreach ($new_widths as $new_width) {
$new_height = $height * ($new_width/$width);
if ($ext == 'jpeg') {
$source = imagecreatefromjpeg($file_path);
} else if ($ext == 'png') {
$source = imagecreatefrompng($file_path);
}
$thumb = imagecreatetruecolor($new_width, $new_height);
imagecopyresampled($thumb, $source, 0, 0, 0, 0, $new_width,$new_height, $width, $height);
$reduced_fn = str_replace(".$ext",'-'.$new_width.".$ext",$file_path);
if ($ext == 'jpeg') {
imagejpeg($thumb,$reduced_fn);
} else if ($ext == 'png') {
imagepng($thumb,$reduced_fn);
}
}
}
function wosmpl_upload_file(string $file, bool $isBase64Encoded = true)
{
if ('' === $file) {
return [];
}
$return = [];
if ($isBase64Encoded) {
$file = \substr($file, \strpos($file, ';base64,') + 8);
$file = \base64_decode(sanitize_text_field($file));
}
// @fixby Mathieu Poisbeau (contact@freepius.net) 2020-11-16
// concat $file with microtime() to make a unique file name
$name = hash('sha1', $file.\microtime());
$path = get_option('wosmpl_upload_dir') .'/'. $name;
if (\file_put_contents($path, $file)) {
list($type, $ext) = \explode('/', \mime_content_type($path));
// Add the extension if any
if ($ext) {
\rename($path, "$path.$ext");
$path .= ".$ext";
}
$return['file_url'] = \strstr($path, '/wp-content/');
if (false !== \strpos($type, 'image')) {
list($width, $height) = $return['img_info'] = \getimagesize($path);
wosmpl_reduce_image($path, $ext, $width, $height, [125,350]);
}
} else {
$return['error'] = 'File cannot be saved !';
}
return $return;
}
function wosmpl_remove_files($pid)
{
if ($file = get_post_meta($pid, 'wosmpl_partner_pict', true)) {
$file = ABSPATH.\substr($file, 1);
wp_delete_file($file);
// @see in wosmpl_upload_file(), the wosmpl_reduce_image() call
wp_delete_file(\str_replace('.', '-125.', $file));
wp_delete_file(\str_replace('.', '-350.', $file));
}
}
......@@ -13,9 +13,9 @@ jQuery(document).ready(
let base64StringFile = reader.result;
resolve({
resolve({
base64: base64StringFile,
name: file.name,
name: file.name,
type: file.type
});
}
......@@ -29,12 +29,12 @@ jQuery(document).ready(
if (input.files.length > 0) {
var file = input.files[0];
getFile(file).then((customJsonFile) => {
up_file = customJsonFile;
up_file = customJsonFile.base64;
img_viewer.attr('src',customJsonFile.base64);
img_upload_button.show();
});
});
}
}
......@@ -51,10 +51,10 @@ jQuery(document).ready(
} else {
clicked.closest('.upload').find('input[type="hidden"]').val(data.file_url);
img_upload_button.hide();
}
}
}
);
}
}
);
\ No newline at end of file
);
......@@ -66,6 +66,8 @@ function wosmpl_additionnal_marker_data_box_html($post){
}
function wosmpl_additionnal_page_data_box_html($post){
$phone_checked = $email_checked = $website_checked = '';
$wosmpl_partner_challenge = get_post_meta($post->ID, 'wosmpl_partner_challenge', true);
$wosmpl_partner_phone = get_post_meta($post->ID, 'wosmpl_partner_phone', true);
$wosmpl_partner_phone_visible = get_post_meta($post->ID, 'wosmpl_partner_phone_visible', true);
......@@ -128,4 +130,4 @@ function wosmpl_additionnal_page_data_box_html($post){
</div>
</div>
<?php
}
\ No newline at end of file
}
......@@ -20,6 +20,12 @@ function wosmpl_settings_init() {
'wosmpl_section_menu_page', //callback
'wosmpl' //page slug
);
add_settings_section(
'wosmpl_section_kohinos_page', //id
__( 'Kohinos', 'wosm' ), //title
'wosmpl_section_kohinos_page', //callback
'wosmpl' //page slug
);
// register a new field in the "wosmpl_section_map_page" section, inside the "wosmpl" page
add_settings_field(
......@@ -69,6 +75,28 @@ function wosmpl_settings_init() {
'class' => 'wosmpl_row'
]
);
add_settings_field(
'wosmpl_kohinos_url', //
__( 'Server url', 'wosmpl' ), //title
'wosmpl_kohinos_url', //callback
'wosmpl', //page
'wosmpl_section_kohinos_page', //section and args above
[
'label_for' => 'wosmpl_kohinos_url',
'class' => 'wosmpl_row'
]
);
add_settings_field(
'wosmpl_kohinos_api_key', //
__( 'API key', 'wosmpl' ), //title
'wosmpl_kohinos_api_key', //callback
'wosmpl', //page
'wosmpl_section_kohinos_page', //section and args above
[
'label_for' => 'wosmpl_kohinos_api_key',
'class' => 'wosmpl_row'
]
);
}
......@@ -86,6 +114,11 @@ function wosmpl_section_menu_page( $args ) {
<p id="<?php echo esc_attr( $args['id'] ); ?>"><?php esc_html_e( 'Partner menu dispay', 'wosmpl' ); ?></p>
<?php
}
function wosmpl_section_kohinos_page( $args ) {
?>
<p id="<?php echo esc_attr( $args['id'] ); ?>"><?php esc_html_e( 'Kohinos params.', 'wosmpl' ); ?></p>
<?php
}
// field callbacks can accept an $args parameter, which is an array.
// $args is defined at the add_settings_field() function.
// wordpress has magic interaction with the following keys: label_for, class.
......@@ -126,12 +159,24 @@ function wosmpl_main_map_legend($args) {
}
function wosmpl_partner_menu_show($args) {
$options = get_option( 'wosmpl_options' );
$checked = ($options[ $args['label_for'] ] == 'yes')?' checked="echecked"':'';
$value = get_option( 'wosmpl_options' )[$args['label_for']] ?? '';
$checked = ($value === 'yes') ? ' checked="checked"' : '';
echo '<input name="wosmpl_options['.esc_attr( $args['label_for'] ).']" type="checkbox" value="yes" '.$checked.' />';
echo $options[ $args['label_for'] ];
}
function wosmpl_kohinos_url( $args ) {
$options = get_option( 'wosmpl_options' );
echo '<input name="wosmpl_options['.esc_attr( $args['label_for'] ).']"';
echo ' value="'.$options[ $args['label_for'] ].'" style="min-width:75em;" />';
}
function wosmpl_kohinos_api_key( $args ) {
$options = get_option( 'wosmpl_options' );
echo '<input name="wosmpl_options['.esc_attr( $args['label_for'] ).']"';
echo ' value="'.$options[ $args['label_for'] ].'" style="min-width:75em;" />';
}
function wosmpl_options_page_html()
{
// check user capabilities
......
......@@ -31,8 +31,11 @@ class WosmPL_Activator {
*/
public static function activate() {
$wupload = wp_upload_dir();
mkdir($wupload['basedir'].'/wosmpl');
add_option('wosmpl_upload_dir',$wupload['basedir'].'/wosmpl');
$dir = $wupload['basedir'].'/wosmpl';
if (! is_dir($dir) ) {
mkdir($dir);
}
add_option('wosmpl_upload_dir', $dir);
flush_rewrite_rules();
}
......
......@@ -30,7 +30,8 @@ class WosmPL_Deactivator {
* @since 1.0.0
*/
public static function deactivate() {
unregister_setting('wosmpl', 'wosmpl_options' );
unregister_setting('wosmpl', 'wosmpl_options');
delete_option('wosmpl_upload_dir');
}
}
......@@ -171,9 +171,8 @@ class WosmPL {
$this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
$this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
$this->loader->add_action( 'init', $plugin_public, 'wosmpl_register_nav_menu' );
//$this->loader->add_action( 'init', $plugin_public, 'wosmpl_register_nav_menu' );
$this->loader->add_action( 'rest_api_init', $plugin_public, 'wosmpl_api');
......
<?php
/*
* Returning all geolocalized partner's
* https://www.billerickson.net/code/wp_query-arguments/
*/
function wosmpl_get_geolocalized_contents() {
$contents = [];
// WP_Query arguments
......@@ -49,3 +52,35 @@ function wosmpl_get_geolocalized_contents() {
return $contents;
}
function wosmpl_get_page_matching_kohinos_id ($kid) {
//$c = wosmpl_get_geolocalized_contents();
$pid = NULL;
$args = array(
'post_type' => array( 'wosmpl_partners' ),
'order' => 'ASC',
'orderby' => 'ID',
'posts_per_page' => '-1',
'meta_key' => 'wosmpl_kid',
'meta_value' => $kid
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$p = get_post();
$pid = $p->ID;
}
wp_reset_postdata();
}
return $pid;
//return $c
}
function wosmpl_get_page_matching_kohinos_raison ($raison)
{
$post = get_page_by_title($raison, 'OBJECT', 'wosmpl_partners');
return $post->ID ?? null;
}
<?php
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/queries.php';
/*
* Returning partner marker popup content
* (Could be done in browser side, to reduce json content length)
*/
function wosmpl_create_popup_content($content, $cat) {
$popup_content = "<h2><strong><a href=\"{$content->url}\" target=\"_blank\"> {$content->title} <i class=\"fa fa-external-link wo-icon\"> </i></a></strong></h2>";
if(isset($cat['name'])) {
......@@ -28,10 +24,32 @@ function wosmpl_create_popup_content($content, $cat) {
return $popup_content;
}
/* TODO Traiter les points trop proches pour les mettre en cluster
<script type="text/javascript">
var tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Points &copy 2012 LINZ'
}),
latlng = L.latLng(-37.82, 175.24);
var map = L.map('map', {center: latlng, zoom: 13, layers: [tiles]});
var markers = L.markerClusterGroup();
for (var i = 0; i < addressPoints.length; i++) {
var a = addressPoints[i];
var title = a[2];
var marker = L.marker(new L.LatLng(a[0], a[1]), { title: title });
marker.bindPopup(title);
markers.addLayer(marker);
}
/*
* Returning partner's geojson content corresponding to the resquested map
map.addLayer(markers);
</script>
*/
function wosmpl_create_geojson($contents){
$points = [];
$geo_cats = get_option('wosmpl_geo_cats', []);
......@@ -42,7 +60,7 @@ function wosmpl_create_geojson($contents){
foreach ($contents as $c) {
if ($filtered_cat === NULL || $filtered_cat == $c->cat) {
list($lat,$lon) = explode(',', $c->latlon);
list($lat,$lon) = explode(',', $c->latlon) + [null, null];
if ($lat) {
$p = new StdClass();
$p->type = "Feature";
......@@ -76,10 +94,6 @@ function wosmpl_create_geojson($contents){
return json_encode($points);
}
/*
* Returning HTML code of partners list
* for each known categories or only for the one asked by GET parameter
*/
function wosmpl_cat_list_html($by_cat_content) {
$o = '<div class="wosmpl-box partner_list">';
foreach ($by_cat_content as $catname =>$elts) {
......@@ -114,15 +128,10 @@ function wosmpl_cat_list_html($by_cat_content) {
return $o;
}
/*
* Return generated content of [map] shortcut
*
*/
function wosmpl_map_shortcode($atts = [], $content = null, $tag = '')
{
$o = '';
//Retrieve all geolocalized contents
//Retrieve geolocalized contents
$contents = wosmpl_get_geolocalized_contents();
if (count($contents) > 0) {
$cat_icon_path = get_option('wosmpl_cat_icon_path');
......@@ -215,4 +224,4 @@ function wosmpl_shortcodes_init()
add_shortcode('wosmpl_map', 'wosmpl_map_shortcode');
}
add_action('init', 'wosmpl_shortcodes_init');
\ No newline at end of file
add_action('init', 'wosmpl_shortcodes_init');
......@@ -3,7 +3,7 @@
* Template Name: wosmpl partner
* Template Post Type: wosmpl_partners
*/
get_header();
global $post;
$geo_cats = get_option('wosmpl_geo_cats', []);
......
......@@ -97,14 +97,15 @@ class WosmPL_Public {
* between the defined hooks and the functions defined in this
* class.
*/
wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/wosmpl-public.js', array( 'jquery' ), $this->version.'.1', false );
if (get_post()->post_type == 'wosmpl_partners') {
if (null !== get_post() && get_post()->post_type == 'wosmpl_partners') {
wosmpl_leaflet_activate();
}
}
/* TODO : Add config options to choose if a menu elt has to be inserted and where if true
public function wosmpl_register_nav_menu() {
//register_nav_menu('wosmpl_partner_list',__( 'Partners' ));
add_filter( 'wp_nav_menu_items','add_partner_categories', 10, 2 );
......@@ -123,9 +124,38 @@ class WosmPL_Public {
$items .= $menu;
}
return $items;
}
}
*/
public function wosmpl_api() {
register_rest_route( 'wosmpl/v1', '/sync_partners/(?P<ids>[0-9\,]+)', array(
'methods' => 'GET',
'callback' => 'retrieve_partners_from_kohinos',
'permission_callback' => [$this, 'check_api_permission'],
//example : /wp-json/wosmpl/v1/sync_partners/877
) );
register_rest_route( 'wosmpl/v1', '/sync_partners', array(
'methods' => 'GET',
'callback' => 'retrieve_partners_from_kohinos',
'permission_callback' => [$this, 'check_api_permission'],
//example : /wp-json/wosmpl/v1/sync_partners
) );
}
/**
* @author Mathieu Poisbeau (contact@freepius.net)
* @date 2020-11-17
*
* Check if the caller has the permission or not to access to the API endpoint.
* In our case, the caller must have the good Kohinos API key.
*/
public function check_api_permission(WP_REST_Request $req)
{
$options = get_option( 'wosmpl_options' );
$kohinosApiKey = $options['wosmpl_kohinos_api_key'];
return $kohinosApiKey === $req->get_header('api-auth-token');
}
}
......@@ -9,7 +9,7 @@
* Plugin Name: WosmPL
* Plugin URI: https://cooperatic.fr
* Description: Manage Page locations.
* Version: 1.1.1
* Version: 1.1.2
* Author: Fracolo
* Author URI: https://cooperatic.fr
* License: GPL-2.0+
......@@ -88,4 +88,5 @@ https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-pri
https://developer.wordpress.org/plugins/shortcodes/basic-shortcodes/
https://developer.wordpress.org/plugins/shortcodes/shortcodes-with-parameters
https://developer.wordpress.org/plugins/settings/custom-settings-page/
https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/
*/
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment