Cannot use a scalar value as an array in my PHP - php

I am actually just started to learn PHP and I use wordpress. I put this code but seems something wrong in it. It shows warning message : Cannot use a scalar value as an array. Can you guys help me with this. Its pretty annoying when I see the warning message on the add new product under the product data table.
Oh, I am using PHP 7.1 now.
I've tried to fix it using PHP checker and searching through the search engine but since I am a beginner, I couldn't find anything that can help.
/**
* Localizes a script, only if the script has already been added.
*
* #since 2.1.0
*
* #param string $handle Name of the script to attach data to.
* #param string $object_name Name of the variable that will contain the data.
* #param array $l10n Array of data to localize.
* #return bool True on success, false on failure.
*/
public function localize( $handle, $object_name, $l10n ) {
if ( $handle === 'jquery' ) {
$handle = 'jquery-core';
}
if ( is_array( $l10n ) && isset( $l10n['l10n_print_after'] ) ) { // back compat, preserve the code in 'l10n_print_after' if present.
$after = $l10n['l10n_print_after'];
unset( $l10n['l10n_print_after'] );
}
foreach ( (array) $l10n as $key => $value ) {
if (! is_scalar( $value ) ) {
continue;
}
$l10n[ $key ] = html_entity_decode( (string) $value, ENT_QUOTES, 'UTF-8' );
}
$script = "var $object_name = " . wp_json_encode( $l10n ) . ';';
if ( ! empty( $after ) ) {
$script .= "\n$after;";
}
$data = $this->get_data( $handle, 'data' );
if ( ! empty( $data ) ) {
$script = "$data\n$script";
}
return $this->add_data( $handle, 'data', $script );
}
Just want to see the warning message not showing up again.
Thanks in advance

Related

Why Wordpress post_name is returning multiple values?

I'm trying to get the current page slug by using get_post_field( 'post_name', get_post() ), however, this is returning multiple values.
My code looks like this (I'm writing this in a custom plugin):
function prefix_filter_query( $query_string, $grid_id, $action ) {
// If the content is not filtered on first render.
if ( 'render' === $action && empty( $query_string ) ) {
$slug = get_post_field( 'post_name', get_post() );
$query_string = [
'categories' => [ $slug ]
];
_error_log($slug);
}
return $query_string;
}
function _error_log ($value) {
error_log(print_r($value, true), 3, __DIR__ . '/log.txt');
error_log("\r\n\r\n", 3, __DIR__ . '/log.txt');
}
add_filter( 'wp_grid_builder/facet/query_string', 'prefix_filter_query', 10, 3 );
The log is showing first the current page (here a category, like 'hoodies'), and then the homepage slug of my website, like this :
hoodies
home
I understood that home was showed because I set the home page of my website to be the static default homepage. I tried to disable it and see if it solved my issue but then the second value returned by the log was just an empty space:
hoodies
I want to get only hoodies and I don't understand why there's a second value, whether it is home or an empty value.
To give a bit of context, I'm using a filter plugin for products in an e-commerce website and the plugin is giving a built-in function to filter the content before it is rendered. https://docs.wpgridbuilder.com/resources/filter-facet-query-string/
Another interesting fact, in our example, hoodies will successfully filter the grid of items to show only hoodies but the query in the URL will be ?_categories=home.
Solution found 28/12/2021
I got an answer from the plugin support (WP Grid Builder) and the issue was that my code was incompatible with the Ajax requests made by WP Grid Builder. Here's the solution I was provided:
add_filter(
'wp_grid_builder/facet/query_string',
function( $query_string, $grid_id, $action ) {
global $post;
if ( 'render' === $action && empty( $query_string ) ) {
$referer = wp_get_referer();
$post_id = wp_doing_ajax() ? url_to_postid( $referer ) : $post->ID;
$post_slug = get_post_field( 'post_name', $post_id );
$query_string = [
'categories' => [ $post_slug ],
];
}
return $query_string;
},
10,
3
);
Are you just trying to get the current page slug ? You could go through the server request uri $_SERVER['REQUEST_URI']:
PHP >= 8.0.0
<?php
/**
* Retrieve the current page slug.
*
* #return String The current page slug.
*/
if ( ! function_exists( 'get_the_current_slug' ) ) {
function get_the_current_slug() {
$url = $_SERVER['REQUEST_URI'];
if ( str_contains( $url, '?' ) ) {
$url = substr( $url, 0, strpos( $url, '?' ) );
};
$slugs = ( str_ends_with( $url, '/' ) ? explode( '/', substr( $url, 1, -1 ) ) : explode( '/', substr( $url, 1 ) ) );
return end( $slugs );
};
};
PHP < 8.0.0 (eg: 7.x.x)
<?php
/**
* Checks if a string ends with a given substring.
*
* #param String $haystack The string to search in.
* #param String $needle The substring to search for in the haystack.
*
* #return Integer < 0 if haystack from position offset is less than needle, > 0 if it is greater than needle, and 0 if they are equal. If offset is equal to (prior to PHP 7.2.18, 7.3.5) or greater than the length of haystack, or the length is set and is less than 0, substr_compare() prints a warning and returns false.
*
* #see https://www.php.net/manual/en/function.substr-compare.php
*/
if ( ! function_exists( 'startsWith' ) ) {
function startsWith( $haystack, $needle ) {
return substr_compare( $haystack, $needle, 0, strlen( $needle ) ) === 0;
};
};
/**
* Retrieve the current page slug.
*
* #return String The current page slug.
*/
if ( ! function_exists( 'get_the_current_slug' ) ) {
function get_the_current_slug() {
$url = $_SERVER['REQUEST_URI'];
if ( strpos( $url, '?' ) !== false ) {
$url = substr( $url, 0, strpos( $url, '?' ) );
};
$slugs = ( startsWith( $url, '/' ) ? explode( '/', substr( $url, 1, -1 ) ) : explode( '/', substr( $url, 1 ) ) );
return end( $slugs );
};
};
On the front end:
<?php
echo get_the_current_slug();

How to add another criteria for product attribute in WordPress /Woocommerce

We have an affiliate-based fashion website with about 30 merchants. I have this code to map product sizes and it works great if the sizing for all merchants is in the same system. However, some merchants use the UK one, some the US, some Italian, some European. The sizes for Men and Women overlap, and also shoe sizes and clothes sizes get mixed up when we try to map them.
So my question is: does anyone know if we can add another parameter to the code below, to also add one of the following, depending on the occasion:
AND merchant name is XYZ.
AND merchant name is NOT XYZ.
AND product category contains Clothing
AND product category does NOT contain Clothing
Below is the current code:
**add_filter( 'dfrpswc_filter_attribute_value', 'mycode_add_size_attribute', 20, 6 );
function mycode_add_size_attribute( $value, $attribute, $post, $product, $set, $action ) {
if ( $attribute != 'pa_size' ) {
return $value;
}
if ( isset( $product['size'] ) ) {
return mycode_get_size( $product['size'], $product['size'] );
}
if ( isset( $product['custom1'] ) ) {
return mycode_get_size( $product['custom1'], $product['custom1'] );
}
if ( isset( $product['name'] ) ) {
return mycode_get_size( $product['name'] );
}
return $value;
}
/**
* A function to normalize size attribute names.
*
* This returns a normalized value of a term based on a supplied
* array of mappings of a key (desired word) mapped to an array of
* keywords (undesired words).
*
* #param string $field The value to normalize.
* #param string $default Optional. What to return if the $field value doesn't have a "normalized" value. Default: ''
*
* #return string Normalized attribute value.
*/
function mycode_get_size( $field, $default = '' ) {
$map = array();
// ++++++++++ Begin Editing Here ++++++++++
$map["XXXS"] = array( "" );
$map["XXS"] = array( "" );
$map["XS"] = array( "" );
// ++++++++++ Stop Editing Here ++++++++++
$terms = array();
foreach ( $map as $key => $keywords ) {
if ( preg_match( '/\b' . preg_quote( $key, '/' ) . '\b/iu', $field ) ) {
$terms[] = $key;
}
foreach ( $keywords as $keyword ) {
if ( preg_match( '/\b' . preg_quote( $keyword, '/' ) . '\b/iu', $field ) ) {
$terms[] = $key;
}
}
}
if ( ! empty( $terms ) ) {
return implode( WC_DELIMITER, array_unique( $terms ) );
}
return $default;
}**
The website is https://fashionfinder.online/ if anyone needs to take a look before replying.
Thank you all so much and kind regards,
Dessislava

How to fix "Warning: preg_match() [function.preg-match]: Compilation failed: nothing to repeat at offset 1" in WordPress

When I installed WooCommerce on a WordPress page I got the chance to manage a little while ago, I started getting these errors whenever I go to a subpage:
Warning: preg_match() [function.preg-match]: Compilation failed: nothing to ?repeat at offset 1 in /var/www/watertours.dk/public_html/wp-includes/class-wp.php on line 222
Warning: preg_match() [function.preg-match]: Compilation failed: nothing to repeat at offset 1 in /var/www/watertours.dk/public_html/wp-includes/class-wp.php on line 223"
It even shows up in the dashboard occasionally.
I have found this guide which I have already tried several times:
step 0: if possible, backup your WP installation folder.
step 1: temporary disable all the plugins (important step)
step 2: in WordPress admin dashboard, go to Settings -> Permalinks
step 3: remember or note down somewhere what you have in the custom permalinks field: http://awesomescreenshot.com/0534epzk0c 96
step 4: temporary enable (switch to) the default permalink: http://awesomescreenshot.com/0f74epyi15 79 Click Save Changes button.
step 5: verify the website is working now (not everything, because the plugins are disabled, but the preg_match error should be gone)
step 6: switch back to the custom permalinks setting you had at step 3
step 7: enable back all the plugins
The error should be gone."
It works for a little while (two minutes or so) and then those two errors start popping up again.
I am thinking of just remaking the WordPress site from the ground up since it is quite a mess anyway. But if anyone has a solution, I would be more than grateful. :)
EDIT:
* Parse request to find correct WordPress query.
*
* Sets up the query variables based on the request. There are also many
* filters and actions that can be used to further manipulate the result.
*
* #since 2.0.0
*
* #global WP_Rewrite $wp_rewrite
*
* #param array|string $extra_query_vars Set the extra query variables.
*/
public function parse_request( $extra_query_vars = '' ) {
global $wp_rewrite;
/**
* Filters whether to parse the request.
*
* #since 3.5.0
*
* #param bool $bool Whether or not to parse the request. Default true.
* #param WP $this Current WordPress environment instance.
* #param array|string $extra_query_vars Extra passed query variables.
*/
if ( ! apply_filters( 'do_parse_request', true, $this, $extra_query_vars ) ) {
return;
}
$this->query_vars = array();
$post_type_query_vars = array();
if ( is_array( $extra_query_vars ) ) {
$this->extra_query_vars = & $extra_query_vars;
} elseif ( ! empty( $extra_query_vars ) ) {
parse_str( $extra_query_vars, $this->extra_query_vars );
}
// Process PATH_INFO, REQUEST_URI, and 404 for permalinks.
// Fetch the rewrite rules.
$rewrite = $wp_rewrite->wp_rewrite_rules();
if ( ! empty( $rewrite ) ) {
// If we match a rewrite rule, this will be cleared.
$error = '404';
$this->did_permalink = true;
$pathinfo = isset( $_SERVER['PATH_INFO'] ) ? $_SERVER['PATH_INFO'] : '';
list( $pathinfo ) = explode( '?', $pathinfo );
$pathinfo = str_replace( '%', '%25', $pathinfo );
list( $req_uri ) = explode( '?', $_SERVER['REQUEST_URI'] );
$self = $_SERVER['PHP_SELF'];
$home_path = trim( parse_url( home_url(), PHP_URL_PATH ), '/' );
$home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) );
// Trim path info from the end and the leading home path from the
// front. For path info requests, this leaves us with the requesting
// filename, if any. For 404 requests, this leaves us with the
// requested permalink.
$req_uri = str_replace( $pathinfo, '', $req_uri );
$req_uri = trim( $req_uri, '/' );
$req_uri = preg_replace( $home_path_regex, '', $req_uri );
$req_uri = trim( $req_uri, '/' );
$pathinfo = trim( $pathinfo, '/' );
$pathinfo = preg_replace( $home_path_regex, '', $pathinfo );
$pathinfo = trim( $pathinfo, '/' );
$self = trim( $self, '/' );
$self = preg_replace( $home_path_regex, '', $self );
$self = trim( $self, '/' );
// The requested permalink is in $pathinfo for path info requests and
// $req_uri for other requests.
if ( ! empty( $pathinfo ) && ! preg_match( '|^.*' . $wp_rewrite->index . '$|', $pathinfo ) ) {
$requested_path = $pathinfo;
} else {
// If the request uri is the index, blank it out so that we don't try to match it against a rule.
if ( $req_uri == $wp_rewrite->index ) {
$req_uri = '';
}
$requested_path = $req_uri;
}
$requested_file = $req_uri;
$this->request = $requested_path;
// Look for matches.
$request_match = $requested_path;
if ( empty( $request_match ) ) {
// An empty request could only match against ^$ regex
if ( isset( $rewrite['$'] ) ) {
$this->matched_rule = '$';
$query = $rewrite['$'];
$matches = array( '' );
}
} else {
foreach ( (array) $rewrite as $match => $query ) {
// If the requested file is the anchor of the match, prepend it to the path info.
if ( ! empty( $requested_file ) && strpos( $match, $requested_file ) === 0 && $requested_file != $requested_path ) {
$request_match = $requested_file . '/' . $requested_path;
}
if ( preg_match( "#^$match#", $request_match, $matches ) || // Line 222
preg_match( "#^$match#", urldecode( $request_match ), $matches ) ) { // Line 223
if ( $wp_rewrite->use_verbose_page_rules && preg_match( '/pagename=\$matches\[([0-9]+)\]/', $query, $varmatch ) ) {
// This is a verbose page match, let's check to be sure about it.
$page = get_page_by_path( $matches[ $varmatch[1] ] );
if ( ! $page ) {
continue;
}
$post_status_obj = get_post_status_object( $page->post_status );
if ( ! $post_status_obj->public && ! $post_status_obj->protected
&& ! $post_status_obj->private && $post_status_obj->exclude_from_search ) {
continue;
}
}
// Got a match.
$this->matched_rule = $match;
break;
}
}
}
if ( isset( $this->matched_rule ) ) {
// Trim the query of everything up to the '?'.
$query = preg_replace( '!^.+\?!', '', $query );
// Substitute the substring matches into the query.
$query = addslashes( WP_MatchesMapRegex::apply( $query, $matches ) );
$this->matched_query = $query;
// Parse the query.
parse_str( $query, $perma_query_vars );
// If we're processing a 404 request, clear the error var since we found something.
if ( '404' == $error ) {
unset( $error, $_GET['error'] );
}
}
// If req_uri is empty or if it is a request for ourself, unset error.
if ( empty( $requested_path ) || $requested_file == $self || strpos( $_SERVER['PHP_SELF'], 'wp-admin/' ) !== false ) {
unset( $error, $_GET['error'] );
if ( isset( $perma_query_vars ) && strpos( $_SERVER['PHP_SELF'], 'wp-admin/' ) !== false ) {
unset( $perma_query_vars );
}
$this->did_permalink = false;
}
}```

How to print to console from a php file in wordpress

I have a php file which is part of a wordpress plugin. I need to debug an issue we are having. I want to find out what a variable's value is. How can I print the variable's value to console? echo or chrome or firefox extensions have been suggested. I couldn't get echo to output to console (echo “$variablename";) and neither using the firephp extension for firefox.
To answer your question, you can do this:
echo '<script>console.log("PHP error: ' . $error . '")</script>';
but I would recommend doing one of the things #Ishas suggested instead. Make sure $error doesn't contain anything that can mess up your script.
If you are thinking about the javascript console, you can not do this from PHP.
You have a few options you could choose from:
echo
var_dump
create a log file
xdebug
For a quick check for a variables value I would use var_dump, it will also show you the data type of the variable. This will be output to the browser when you request the page.
Logging to the DevTools console from PHP in WordPress
Here you can see my solution for the problem in action while debugging coupon logic in WooCommerce. This solution is meant for debug purposes, only. (Note: Screenshot not up to date, it will also expose private members.)
Features
Allow printing before and after rendering has started
Works in front-end and back-end
Print any amount of variables
Encode arrays and objects
Expose private and protected members of objects
Also log to the log file
Safely and easily opt-out in the production environment (in case you keep the calls)
Print the caller class, function and hook (quality of life improvement)
Solution
wp-debug.php
function console_log(): string {
list( , $caller ) = debug_backtrace( false );
$action = current_action();
$encoded_args = [];
foreach ( func_get_args() as $arg ) try {
if ( is_object( $arg ) ) {
$extract_props = function( $obj ) use ( &$extract_props ): array {
$members = [];
$class = get_class( $obj );
foreach ( ( new ReflectionClass( $class ) )->getProperties() as $prop ) {
$prop->setAccessible( true );
$name = $prop->getName();
if ( isset( $obj->{$name} ) ) {
$value = $prop->getValue( $obj );
if ( is_array( $value ) ) {
$members[$name] = [];
foreach ( $value as $item ) {
if ( is_object( $item ) ) {
$itemArray = $extract_props( $item );
$members[$name][] = $itemArray;
} else {
$members[$name][] = $item;
}
}
} else if ( is_object( $value ) ) {
$members[$name] = $extract_props( $value );
} else $members[$name] = $value;
}
}
return $members;
};
$encoded_args[] = json_encode( $extract_props( $arg ) );
} else {
$encoded_args[] = json_encode( $arg );
}
} catch ( Exception $ex ) {
$encoded_args[] = '`' . print_r( $arg, true ) . '`';
}
$msg = '`📜`, `'
. ( array_key_exists( 'class', $caller ) ? $caller['class'] : "\x3croot\x3e" )
. '\\\\'
. $caller['function'] . '()`, '
. ( strlen( $action ) > 0 ? '`🪝`, `' . $action . '`, ' : '' )
. '` ➡️ `, ' . implode( ', ', $encoded_args );
$html = '<script type="text/javascript">console.log(' . $msg . ')</script>';
add_action( 'wp_enqueue_scripts', function() use ( $html ) {
echo $html;
} );
add_action( 'admin_enqueue_scripts', function() use ( $html ) {
echo $html;
} );
error_log( $msg );
return $html;
}
wp-config.php (partially)
// ...
define( 'WP_DEBUG', true );
// ...
/** Include WP debug helper */
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && file_exists( ABSPATH . 'wp-debug.php' ) ) {
include_once ABSPATH . 'wp-debug.php';
}
if ( ! function_exists( 'console_log' ) ) {
function console_log() {
}
}
/** Sets up WordPress vars and included files. */
require_once( ABSPATH . 'wp-settings.php' );
Usage
Before the HTML <head> is rendered:
console_log( $myObj, $myArray, 123, "test" );
After the HTML <head> is rendered (in templates, etc. / use when the above does not work):
echo console_log( $myObj, $myArray, 123, "test" );
Output format
📜 <caller class>\<caller function>() 🪝 <caller action/hook> ➡️ <variables ...>
Special thanks to
Andre Medeiros for the property extraction method
You can write a utility function like this:
function prefix_console_log_message( $message ) {
$message = htmlspecialchars( stripslashes( $message ) );
//Replacing Quotes, so that it does not mess up the script
$message = str_replace( '"', "-", $message );
$message = str_replace( "'", "-", $message );
return "<script>console.log('{$message}')</script>";
}
The you may call the function like this:
echo prefix_console_log_message( "Error Message: This is really a 'unique' problem!" );
and this will output to console like this:
Error Message: This is really a -unique- problem!
Notice the quotes replaced with "-". It is done so that message does not mess up your script as pointed by #Josef-Engelfrost
You may also go one step further and do something like this:
function prefix_console_log_message( $message, $type = "log" ) {
$message_types = array( 'log', 'error', 'warn', 'info' );
$type = ( in_array( strtolower( $type ), $message_types ) ) ? strtolower( $type ) : $message_types[0];
$message = htmlspecialchars( stripslashes( $message ) );
//Replacing Quotes, so that it does not mess up the script
$message = str_replace( '"', "-", $message );
$message = str_replace( "'", "-", $message );
return "<script>console.{$type}('{$message}')</script>";
}
and call the function like this:
echo prefix_console_log_message( "Error Message: This is really a 'unique' problem!" , 'error');
It will output error in console.

Parse error: syntax error, unexpected $end in /wp-includes/class-http.php on line 1137

So I've combed through the forums and it seems that I most likely have a bracket or something missing.
This is for my wordpress site. It's actually a Multisite if that makes any difference.
The funny thing is I have fixed this issue before by simply replacing this class-http.php file with a new one from the latest Wordpress download. But here I am about a week and a half later with the same error happening again, so maybe there is something else too it that someone is abreast of?
---Line 1137 is the laste line of code - it already seems to my untrained eye it's missing something. But even more important, what is stripping this out at a later date?
Also, I just looked at the fresh new class-http.php file and it seems to have 2174 lines as opposed to just 1137. I wonder maybe a plugin? or what could be continually deprecating this file?
fclose( $stream_handle );
} else {
$header_length = 0;
while ( ! feof( $handle ) && $keep_reading ) {
$block = fread( $handle, $block_size );
$strResponse .= $block;
if ( ! $bodyStarted && strpos( $strResponse, "\r\n\r\n" ) ) {
$header_length = strpos( $strResponse, "\r\n\r\n" ) + 4;
$bodyStarted = true;
}
$keep_reading = ( ! $bodyStarted || !isset( $r['limit_response_size'] ) || strlen( $strResponse ) < ( $header_length + $r['limit_response_size'] ) );
}
$process = WP_Http::processResponse( $strResponse );
unset( $strResponse );
}
fclose( $handle );
$arrHeaders = WP_Http::processHeaders( $process['headers'], $url );
$response = array(
'headers' => $arrHeaders['headers'],
// Not yet processed.
'body' => null,
'response' => $arrHeaders['response'],
'cookies' => $arrHeaders['cookies'],
'filename' => $r['filename']
);
// Handle redirects.
if ( false !== ( $redirect_response = WP_HTTP::handle_redirects( $url, $r, $response ) ) )
return $redirect_response;
// If the body was chunk encoded, then decode it.
if ( ! empty( $process['body'] ) && isset( $arrHeaders['headers']['transfer-encoding'] ) && 'chunked' == $arrHeaders['headers']['transfer-encoding'] )
$process['body'] = WP_Http::chunkTransferDecode($process['body']);
if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($arrHeaders['headers']) )
$process['body'] = WP_Http_Encoding::decompress( $process['body'] );
if ( isset( $r['limit_response_size'] ) && strlen( $process['body'] ) > $r['limit_response_size'] )
$process['body'] = substr( $process['body'], 0, $r['limit_response_size'] );
$response['body'] = $process['body'];
return $response;
}
/**
* Verifies the received SSL certificate against it's Common Names and subjectAltName fields
*
* PHP's SSL verifications only verify that it's a valid Certificate, it doesn't verify if
* the certificate is valid for the hostname which was requested.
* This function verifies the requested hostname against certificate's subjectAltName field,
* if that is empty, or contains no DNS entries, a fallback to the Common Name field is used.
*
* IP Address support is included if the request is being made to an IP address.
*
* #since 3.7.0
* #static
*
* #param stream $stream The PHP Stream which the SSL request is being made over
* #param string $host The hostname being requested
* #return bool If the cerficiate presented in $stream is valid for $host
*/
public static function verify_ssl_certificate( $stream, $host ) {
$context_options = stream_context_get_options( $stream );
if ( empty( $context_options['ssl']['peer_certificate'] ) )
return false;
$cert = openssl_x509_parse( $context_options['ssl']['peer_certificate'] );
if ( ! $cert )
return false;
/*
* If the request is being made to an IP address, we'll validate against IP fields
* in the cert (if they exist)
*/
$host_type = ( WP_HTTP::is_ip_address( $host ) ? 'ip' : 'dns' );
$certificate_hostnames = array();
if ( ! empty( $cert['extensions']['subjectAltName'] ) ) {
$match_against = preg_split( '/,\s*/', $cert['extensions']['subjectAltName'] );
foreach ( $match_against as $match ) {
list( $match_type, $match_host ) = explode( ':', $match );
if ( $host_type == strtolower( trim( $match_type ) ) ) // IP: or DNS:
$certificate_hostnames[] = strtolower( trim( $match_host ) );
}
} elseif ( !empty( $cert['subject']['CN'] ) ) {
// Only use the CN when the certificate includes no subjectAltName extension.
$certificate_hostnames[] = strtolower( $cert['subject']['CN'] );
}
// Exact hostname/IP matches.
if ( in_array( strtolower( $host ), $certificat

Categories