• Session Fixation is a security vulnerability where an attacker “fixes” a user’s session ID before the user even logs in. If the application doesn’t change the session ID upon authentication, the attacker can use that pre-set ID to hijack the user’s account.
  • Attacker visits target website and obtains valid session ID. Attacker traps victim to login with that session ID. http://example.com/?PHPSESSID=123. Because the server did not issue a new session ID after login, the old ID (123) is now associated with the victim’s authenticated account.
  • Modern WordPress (version 4.0 and later) has moved away from standard PHP native sessions (PHPSESSID) and implemented a much more robust system that specifically defends against fixation.
  • WordPress uses a custom session manager. When a user logs in, WordPress generates a brand-new, random Session Token. Even if an attacker managed to plant a cookie on a user’s browser beforehand, that cookie is ignored or overwritten during the wp_signon() process. Session token stored in wp_usermeta table for that user. WordPress then sends that token to your browser wrapped inside a cookie (typically named wordpress_logged_in_[hash]).

  • Primitive Capabilities: These are “hard” permissions stored in the database for a specific role (e.g., edit_posts, manage_options). They are a simple “Yes” or “No.”
  • Meta Capabilities: These are “contextual” permissions that depend on a specific object, usually a Post ID (e.g., edit_post, delete_post).****
  • When you call current_user_can('edit_post', 123), WordPress runs map_meta_cap. If Post 123 belongs to the current user, it maps the request to the primitive edit_posts. If Post 123 belongs to someone else, it maps it to edit_others_posts.
/**
 * Custom Permission Logic: 
 * Allow users to edit a post if they are the "Project Lead" 
 * via a custom meta field.
 */
add_filter( 'map_meta_cap', 'my_custom_project_permissions', 10, 4 );

function my_custom_project_permissions( $caps, $cap, $user_id, $args ) {
    
    // 1. Target the specific meta capability
    if ( 'edit_post' === $cap || 'delete_post' === $cap ) {
        
        $post_id = $args[0]; // The ID of the post being checked
        $post_type = get_post_type( $post_id );

        // 2. Only apply logic to our 'project' post type
        if ( 'project' === $post_type ) {
            
            // Fetch the assigned "Lead" from post meta
            $project_lead_id = get_post_meta( $post_id, '_project_lead_id', true );

            // 3. If the current user is the Lead, give them the 'exist' capability
            // 'exist' is a primitive cap everyone has, effectively granting permission.
            if ( (int) $project_lead_id === (int) $user_id ) {
                return array( 'exist' );
            }
        }
    }

    // Return the original capabilities for all other cases
    return $caps;
}
  • Never return an empty array if you want to deny access. If you return an empty array, WordPress assumes no primitive capabilities are required, and the check will pass! To deny access, you should return a capability that you know the user doesn’t have, such as 'do_not_allow'.

  • WP calls hooks internally.

// This is how WordPress 'fires' the filter to let you change things
$caps = apply_filters( 'map_meta_cap', $caps, $cap, $user_id, $args );

flush_rewrite_rules( $hard ) It clears the rewrite rules stored in the database and then regenerates them based on the current active themes and plugins. Global Helper function

  • The $hard parameter: * true (default): Updates the database and attempts to overwrite the physical .htaccess file (on Apache) or the web.config file (on IIS).
  • false: Only updates the rules in the database (useful if you only have custom “soft” rules that don’t need server-level changes).

$wp_rewrite->flush_rules() It handles the specific logic of clearing the rules from the rewrite_rules option in the wp_options table. Class Method


Changing mail format

  • wp_mail_content_type filter
// 1. Define a function to set the content type
function set_html_content_type() {
    return 'text/html';
}

// 2. Add the filter
add_filter( 'wp_mail_content_type', 'set_html_content_type' );

$to          = 'user@example.com';
$subject     = 'Welcome to the Site!';
$body        = '<h1>Hello!</h1><p>Thanks for joining our <strong>awesome</strong> community.</p>';

// 3. Send the email
$sent = wp_mail( $to, $subject, $body );

// WITH ATTACHMENTS AND HEADER
$mail_sent = wp_mail( $to, $subject, $message, $headers, $attachments );

// 4. RESET the filter so other emails stay as plain text
remove_filter( 'wp_mail_content_type', 'set_html_content_type' );

In the WordPress Customizer (the “Customize” screen), a Control is the bridge between a setting in the database and the UI element the user interacts with (like a color picker, a checkbox, or a text input).

  • The PHP Side: You define a control using $wp_customize->add_control().
  • The JS Side: Every PHP control is mirrored by a JavaScript Class instance (wp.customize.Control).

Why standard DOM Ready (jQuery(document).ready) fails? The Customizer is Asynchronous: Sections and controls in the Customizer are often loaded dynamically. Some might not even exist in the DOM when document.ready fires. WordPress uses jQuery.Deferred objects to manage the asynchronous nature of the UI. control.deferred.embedded is a “promise” that represents the specific moment the control’s HTML is actually placed into the DOM.

DOM ready means: “The page HTML is loaded”

control.deferred.embedded.done() means: “This specific control is fully constructed, rendered, and inserted into the Customizer UI”


  • “chunks” are the final bundled files generated from your source code. The distinction between initial and non-initial chunks boils down to when and how the browser loads them.
  • The first initial chunk loaded typically contains the Webpack Runtime—the small piece of logic that enables Webpack to connect all other modules and chunks together. Script tag / preload.
  • If your config has entry: { main: './src/index.js' }, Webpack creates an initial chunk named main.js.
  • Non-initial chunks (often called Async Chunks) are files that are loaded on-demand while the application is already running. Not loaded at startup. Loaded via JavaScript at runtime. Improves initial load performance

Restricting access to wp posts api

add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) {
    // Target the specific posts endpoint
    if ( '/wp/v2/posts' === $request->get_route() ) {
        // If the user isn't logged in, return a 401 Unauthorized error
        if ( ! is_user_logged_in() ) {
            return new WP_Error( 
                'rest_forbidden', 
                __( 'You do not have permission to view posts via the API.' ), 
                array( 'status' => 401 ) 
            );
        }
    }
    return $result;
}, 10, 3 );
  • or unregister the route
add_action( 'rest_api_init', function() {
    // This removes the 'posts' route from the 'wp/v2' namespace
    register_rest_route( 'wp/v2', '/posts', array(
        'callback' => '__return_false', // This effectively kills the route
    ) );
    
    // Alternatively, use the internal unregister function (WP 4.4+)
    global $wp_rest_server;
    $wp_rest_server->deregister_route( 'wp/v2', '/posts' );
}, 20 );

rest_pre_dispatch filter to intercept the request before it is processed.


  • wp_posts Table: Stores “Top-level” data that every post must have (ID, Title, Content, Status, Dates, Author).

  • wp_postmeta Table: Stores “Extra” or “Custom” data (Price, Location, SEO descriptions, or custom fields you create).

$post_id = 123; // The ID of the post you want to change
$new_date = '2023-01-15 10:30:00'; // YYYY-MM-DD HH:MM:SS

$my_post = array(
    'ID'            => $post_id,
    'post_date'     => $new_date,
    'post_date_gmt' => get_gmt_from_date( $new_date ), // Converts local to UTC
    'post_modified'     => '2024-05-01 14:00:00',
    'post_modified_gmt' => get_gmt_from_date( '2024-05-01 14:00:00' )
);

// Update the post in the database
wp_update_post( $my_post );
Function PrefixPurposeExamples
wp_..._postMain content (posts/pages).wp_insert_post(), get_post()
update_..._metaAdding extra info to objects.update_user_meta(), update_term_meta()
wp_..._userManaging site members.wp_create_user(), wp_update_user()
wp_..._commentManaging the comment section.wp_insert_comment(), wp_set_comment_status()

rest_api_init This is the “registration” hook. It is the very first thing that fires when a request hits the /wp-json/ prefix. rest_authentication_errors

This is the “bouncer” at the door. It runs after the bas

rest_pre_dispatch This is the “intercept” hook. It is the last chance to stop the default WordPress logic from running.

rest_pre_insert_{post_type} This is the “data mapper” hook. It is specific to the default Content Endpoints (Posts, Pages, Custom Post Types). Executed after the request is validated but before wp_insert_post() is called.

rest_prepare_{post_type} This is the “formatter” hook. It runs after the data has been retrieved from the database but before it is turned into JSON.

rest_post_dispatch This is the “outgoing” hook. The work is done, and the response is ready to leave the server.


  1. before_wp_load: Fires before WordPress is loaded.
  2. after_wp_load: Fires once wp-settings.php has finished, meaning WordPress is fully “up.”
  3. Command Execution: This is where your actual command (like exporting the DB) happens.

PDO is a database abstraction layer. It acts as a consistent interface for 12 different database drivers (MySQL, PostgreSQL, SQLite, MS SQL, etc.). As the name suggests, MySQLi is built specifically for MySQL

FeaturePDOMySQLi
Database Support12 different driversMySQL Only
API StyleObject-Oriented onlyObject-Oriented and Procedural
Named ParametersYes (:id, :name)No (Uses ? only)
Prepared StatementsClient-side emulation availableServer-side only
PerformanceSlightly slower (abstraction overhead)Slightly faster (direct driver)
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email AND status = :status");
$stmt->execute(['email' => $user_email, 'status' => 'active']);
// mysqli
$stmt = $db->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->bind_param("ss", $user_email, $status);
$stmt->execute();

Under the hood, $wpdb uses MySQLi (it switched from the old mysql extension in version 3.9).


Use autoload = ‘yes’ if…Use autoload = ’no’ if…
The data is needed on every page load (e.g., site title, active theme, core settings).The data is only used on specific pages (e.g., a specific plugin’s settings page).
The data is small (a few strings or a small array).The data is large (huge serialized arrays, long lists of IDs, or logs).

The has_filters() function (and its sibling has_filter()) is a diagnostic tool used to check if a specific hook has any functions attached to it. While

  • has_filter( 'hook_name' ): Returns true if any functions are attached, false if not.

  • has_filter( 'hook_name', 'function_to_check' ): Returns the priority (integer) of that specific function if it’s attached to the hook, or false if it’s not.


Returning vs echoing in shortcode callback Because WordPress processes shortcodes before it finishes rendering the full page layout, your content will “jump” to the very top of the page, appearing above the header or outside of its intended CSS container.

  • wp_kses_post($data): Allows only the HTML tags that are permitted in a standard WordPress post. Best for outputting user-generated content or custom fields that allow basic formatting.

  • wp_kses($data, $allowed_html): The most restrictive and safest. You must explicitly define exactly which tags and attributes (like href or class) are allowed.

  • Escaping (esc_html(), esc_attr()): These are output-only functions. They turn HTML into harmless text strings. You use these at the “moment of echo.”


  • gmdate(): Standard PHP function. It returns the time in GMT (UTC) regardless of your WordPress site settings.

  • date_i18n(): The WordPress-localized version. It translates the month/day names into the site’s language and adjusts the time based on the Timezone set in Settings > General.


shortcode

add_shortcode( 'my_button', 'render_my_button' );

function render_my_button( $atts, $content = null, $tag = '' ) {
    // 1. Merge user attributes with defaults
    $attributes = shortcode_atts( array(
        'url'   => '#',
        'color' => 'blue',
    ), $atts, $tag );

    // 2. Build the output
    // Use the $tag as a class name for extra flexibility
    $output = '<a href="' . esc_url( $attributes['url'] ) . '" class="btn btn-' . esc_attr( $attributes['color'] ) . ' ' . esc_attr( $tag ) . '">';
    
    // 3. Handle the enclosed content
    $output .= $content ? $content : 'Click Here';
    
    $output .= '</a>';

    return $output;
}
  • To execute nested shortcode, $inner_content = do_shortcode( $content );

The Options API is the simplest way to store and retrieve data in the WordPress database (specifically the wp_options table). It consists of four main functions: add_option(), get_option(), update_option(), and delete_option(). The Settings API is a standardized way for developers to create administrative settings pages. It is essentially a wrapper that sits on top of the Options API.

/**
 * 1. Register the menu page
 */
add_action( 'admin_menu', 'my_plugin_register_menu_page' );

function my_plugin_register_menu_page() {
    add_menu_page(
        'Plugin Settings Page',      // Page Title
        'My Plugin',                 // Menu Title
        'manage_options',            // Capability
        'my-plugin-settings',        // Menu Slug
        'my_plugin_render_page_html',// Callback Function
        'dashicons-admin-settings',  // Icon
        80                           // Position
    );
}

/**
 * 2. Render the HTML (Where the Settings API lives)
 */
function my_plugin_render_page_html() {
    ?>
    <div class="wrap">
        <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
        <form action="options.php" method="post">
            <?php
            // These functions belong to the Settings API:
            settings_fields( 'my_plugin_options_group' );
            do_settings_sections( 'my-plugin-settings' );
            submit_button( 'Save Changes' );
            ?>
        </form>
    </div>
    <?php
}
  • register post type
add_action( 'init', 'my_custom_post_type' );

function my_custom_post_type() {
    $args = array(
        'labels'      => array(
            'name'          => 'Books',
            'singular_name' => 'Book',
        ),
        'public'      => true,
        'has_archive' => true,
        'menu_icon'   => 'dashicons-book', // Sidebar icon
        'supports'    => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
        'show_in_rest' => true, // Set to true to enable the Block Editor (Gutenberg)
        'rewrite'     => array( 'slug' => 'books' ),
    );

    register_post_type( 'book', $args );
}
add_action( 'init', 'my_custom_taxonomy' );

function my_custom_taxonomy() {
    $args = array(
        'labels'       => array(
            'name'          => 'Genres',
            'singular_name' => 'Genre',
        ),
        'hierarchical' => true, // true = like Categories (checkboxes); false = like Tags
        'show_ui'      => true,
        'show_in_rest' => true,
        'rewrite'      => array( 'slug' => 'genre' ),
    );

    // This taxonomy is linked to the 'book' post type
    register_taxonomy( 'genre', array( 'book' ), $args );
}
  • func get srgs
// Note the '99' for high priority and '10' to allow up to 10 arguments
add_action( 'save_post', 'my_dynamic_inspector', 99, 10 );

function my_dynamic_inspector() {
    $args = func_get_args();
    
    error_log( 'Hook triggered! Arguments found: ' . count( $args ) );
    
    foreach ( $args as $index => $arg ) {
        error_log( "Argument [$index]: " . print_r( $arg, true ) );
    }
}
add_action( 'add_meta_boxes', 'my_plugin_register_meta_box' );

function my_plugin_register_meta_box() {
    add_meta_box(
        'my_custom_field_id',      // ID
        'Additional Information',   // Title
        'my_plugin_render_box',     // Callback
        'post',                     // Screen (Post Type)
        'side',                     // Context (Sidebar)
        'default'                   // Priority
    );
}
function my_enqueue_scripts() {
    // 1. Register the script
    wp_register_script( 'my-helper-js', get_template_directory_uri() . '/js/helper.js', array('jquery'), '1.0', true );

    // 2. Localize it with data
    $script_data = array(
        'ajax_url' => admin_url( 'admin-ajax.php' ),
        'user_name' => wp_get_current_user()->display_name,
        'alert_msg' => __( 'Are you sure you want to delete this?', 'my-text-domain' )
    );
    wp_localize_script( 'my-helper-js', 'myData', $script_data );

    // 3. Enqueue the script
    wp_enqueue_script( 'my-helper-js' );
}
add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );
FeatureTemplate TagsConditional Tags
Primary GoalTo display or fetch content.To check a condition.
Return ValueUsually HTML/String (or void if echoing).Always Boolean (true or false).
Usage ContextPrimarily inside the HTML of a template.Primarily inside if statements.
Examplethe_author();if ( is_home() ) { ... }

Developers can modify the default lifespan of nonces by hooking into the nonce_life filter and returning a new lifetime duration specified in seconds check_ajax_referrer() wp_nonce_field() wp_nonce_url()


When executing, sanitize_text_field() checks for invalid UTF-8 encoding, converts single less-than (<) characters into HTML entities, strips out all HTML tags, removes line breaks, tabs, and extra whitespace, and strips octets.

The wp_send_json_success() function JSON-encodes your PHP response, prints it, and then dies. This automatically triggers the appropriate done() success callback to fire in your client-side JavaScript

dbdelta()

function my_plugin_create_table() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'my_custom_log';
    $charset_collate = $wpdb->get_charset_collate();

    // Note the strict formatting: 
    // - New line for every field
    // - Uppercase keywords
    // - Primary Key spacing
    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
        user_name tinytext NOT NULL,
        text text NOT NULL,
        PRIMARY KEY  (id)
    ) $charset_collate;";

    require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
    dbDelta( $sql );
}

register_activation_hook( __FILE__, 'my_plugin_create_table' );

The user_register action hook fires whenever a new user is created (when the user ID does not yet exist), allowing developers to programmatically insert default user metadata during the registration process.

get_theme_file_path() returns the active (current) theme’s directory path, automatically falling back to the parent theme if a file doesn’t exist in an active child theme. get_parent_theme_file_path() explicitly returns the directory path of the parent theme regardless of child theme files.


get_stylesheet_uri() returns the active theme’s style.css file URL. get_theme_file_uri() returns the active theme’s URL for a specified file, but safely falls back to the parent theme if the child theme doesn’t have the file.

load_theme_textdomain() is used by parent themes to load translations, whereas load_child_theme_textdomain() is specifically designed to load translations mapped to a unique text domain for a child theme.




  • some filters
  • some actions
  • object cache, advanced cache, etc
  • wordpress hierarchy
  • the_title_attribute()
  • nonces
  • n+1 query
  • wp db tables and relations