expires_at ) || '0000-00-00 00:00:00' === $quote->expires_at ) { return false; } return strtotime( $quote->expires_at ) < current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested } /** * Uppercase with multibyte support when available. * * @param string $s String. * @return string */ public static function upper( $s ) { return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( (string) $s ) : strtoupper( (string) $s ); } /** * Translated status labels. * * @return array */ public static function status_labels() { return array( 'pending' => __( 'Pending', 'nuxwp-devis' ), 'quoted' => __( 'Quoted', 'nuxwp-devis' ), 'accepted' => __( 'Accepted', 'nuxwp-devis' ), 'rejected' => __( 'Declined', 'nuxwp-devis' ), 'expired' => __( 'Expired', 'nuxwp-devis' ), 'converted' => __( 'Ordered', 'nuxwp-devis' ), ); } /** * Access level of the current visitor: guest | denied | allowed. * * @param int|null $user_id User ID (defaults to current). * @return string */ public static function user_access( $user_id = null ) { $user_id = $user_id ? (int) $user_id : get_current_user_id(); if ( ! $user_id ) { return 'guest'; } return self::user_can_request( $user_id ) ? 'allowed' : 'denied'; } /** * Can this user request a quote? * * @param int $user_id User ID. * @return bool */ public static function user_can_request( $user_id ) { $user = get_userdata( (int) $user_id ); if ( ! $user ) { return false; } $roles = DevisQR_Settings::allowed_roles(); $can = true; if ( null !== $roles ) { $can = (bool) array_intersect( (array) $user->roles, $roles ); if ( ! $can && user_can( $user, 'manage_woocommerce' ) ) { $can = true; } } /** * Filter whether a user may request a quote. * * @param bool $can Result. * @param WP_User $user User. */ return (bool) apply_filters( 'devisqr_user_can_request', $can, $user ); } /** * Minimum cart total (store currency, incl. tax). 0 = no threshold. * * @return float */ public static function min_cart_total() { /** * Filter the minimum cart total required to request a quote. * * @param float $min Minimum. */ return (float) apply_filters( 'devisqr_min_cart_total', (float) DevisQR_Settings::get( 'min_cart_total' ) ); } /** * Contact link HTML ("contact us"), from the contact URL or e-mail setting. * * @return string */ public static function contact_link() { $s = DevisQR_Settings::resolved(); $href = $s['contact_url'] ? $s['contact_url'] : 'mailto:' . $s['contact_email']; return '' . esc_html__( 'contact us', 'nuxwp-devis' ) . ''; } /** * Nonce-protected download URL for a quote file. * * @param int $quote_id Quote ID. * @param string $which pdf|attachment. * @return string */ public static function download_url( $quote_id, $which = 'pdf' ) { $args = array( 'devisqr_download' => (int) $quote_id, '_wpnonce' => wp_create_nonce( 'devisqr_download_' . (int) $quote_id ), ); if ( 'attachment' === $which ) { $args['devisqr_file'] = 'attachment'; } return add_query_arg( $args, home_url( '/' ) ); } /** * Stream a file and exit. * * @param string $path Absolute path (already validated). * @param string $filename Download name. * @param string $mime MIME type. */ public static function serve_file( $path, $filename, $mime ) { while ( ob_get_level() ) { // Stray output from other plugins would corrupt the file. ob_end_clean(); } nocache_headers(); header( 'Content-Type: ' . $mime ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); header( 'Content-Length: ' . filesize( $path ) ); header( 'X-Content-Type-Options: nosniff' ); readfile( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile exit; } /** * Allowed roles as [slug => name] for the settings UI. * * @return array */ public static function role_choices() { $out = array(); foreach ( wp_roles()->roles as $slug => $role ) { $out[] = array( 'slug' => $slug, 'name' => translate_user_role( $role['name'] ), ); } return $out; } /** * Currency/number settings for the JS apps. * * @return array */ public static function js_currency() { return array( 'symbol' => html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' ), 'position' => get_option( 'woocommerce_currency_pos', 'left' ), 'decimals' => wc_get_price_decimals(), 'decimalSep' => wc_get_price_decimal_separator(), 'thousandSep' => wc_get_price_thousand_separator(), ); } /* ------------------------------------------------------------------ */ /* Logo (PDF + e-mails) */ /* ------------------------------------------------------------------ */ const BRANDING_DIR = 'devisqr-branding'; /** * Logo for the PDF and the e-mails: the "PDF logo" setting, else the theme logo. * PNG/JPEG are used as they are; any other image (WebP, GIF, AVIF…) is converted once to PNG * in uploads/devisqr-branding/ (public, so e-mail clients can load it) and cached by attachment + file version. * * @param bool $reset Ignore the per-request cache (after a settings save). * @return array|null { path, url } or null when there is no usable logo. */ public static function logo( $reset = false ) { static $cache = false; if ( $reset ) { $cache = false; } if ( false !== $cache ) { return $cache; } $cache = null; delete_transient( 'devisqr_logo_warning' ); $id = (int) DevisQR_Settings::get( 'pdf_logo_id' ); if ( ! $id ) { $id = (int) get_theme_mod( 'custom_logo' ); } if ( ! $id ) { return null; } $src = get_attached_file( $id ); if ( ! $src || ! file_exists( $src ) ) { self::logo_warning( __( 'The selected logo file is missing from the uploads folder.', 'nuxwp-devis' ) ); return null; } $type = wp_check_filetype( $src ); if ( in_array( $type['type'], array( 'image/png', 'image/jpeg' ), true ) ) { // PDF: the "large" intermediate size when it exists (keeps the PDF writer's memory low on huge originals). $large = image_get_intermediate_size( $id, 'large' ); $path = ( $large && ! empty( $large['path'] ) ) ? trailingslashit( wp_upload_dir()['basedir'] ) . $large['path'] : $src; $medium = wp_get_attachment_image_src( $id, 'medium' ); $cache = array( 'path' => file_exists( $path ) ? $path : $src, 'url' => $medium ? $medium[0] : wp_get_attachment_url( $id ) ); return $cache; } // Only raster formats are converted (no SVG/PDF rasterising through Imagick). if ( ! wp_attachment_is_image( $id ) || ! in_array( $type['type'], array( 'image/webp', 'image/gif', 'image/avif', 'image/bmp' ), true ) ) { /* translators: %s: image format, e.g. image/svg+xml */ self::logo_warning( sprintf( __( '%s logos are not supported; choose a PNG, JPEG, WebP, GIF or AVIF image.', 'nuxwp-devis' ), $type['type'] ? $type['type'] : pathinfo( $src, PATHINFO_EXTENSION ) ) ); return null; } // Convert to PNG (once per attachment/file version). $upload = wp_upload_dir(); $dir = trailingslashit( $upload['basedir'] ) . self::BRANDING_DIR; $url = trailingslashit( $upload['baseurl'] ) . self::BRANDING_DIR; $name = 'logo-' . $id . '-' . substr( md5( $src . '|' . filemtime( $src ) . '|' . filesize( $src ) ), 0, 12 ) . '.png'; $path = $dir . '/' . $name; if ( ! file_exists( $path ) ) { // A failed conversion is remembered for this exact source file: no retry on every PDF/e-mail until the logo changes. $failed = get_transient( 'devisqr_logo_failed' ); if ( is_array( $failed ) && isset( $failed['name'], $failed['msg'] ) && $failed['name'] === $name ) { self::logo_warning( $failed['msg'] ); return null; } if ( ! wp_mkdir_p( $dir ) ) { self::logo_failed( $name, __( 'The uploads folder is not writable; the logo could not be converted.', 'nuxwp-devis' ) ); return null; } if ( ! file_exists( $dir . '/index.php' ) ) { file_put_contents( $dir . '/index.php', 'resize( 600, 600, false ); $saved = $editor->save( $path, 'image/png' ); if ( is_wp_error( $saved ) || empty( $saved['path'] ) || ! file_exists( $saved['path'] ) ) { self::logo_failed( $name, __( 'The logo could not be converted to PNG; choose a PNG or JPEG logo.', 'nuxwp-devis' ) ); return null; } // Previous conversions are kept on purpose: e-mails already sent link to them. delete_transient( 'devisqr_logo_failed' ); $path = $saved['path']; } $cache = array( 'path' => $path, 'url' => $url . '/' . basename( $path ) ); return $cache; } /** * Remember why the logo is unusable (shown in Settings → Company & PDF). * * @param string $message Message. */ private static function logo_warning( $message ) { set_transient( 'devisqr_logo_warning', $message, DAY_IN_SECONDS ); } /** * Remember a failed conversion for a given source version (no retry storm) and show the warning. * * @param string $name Target file name (encodes attachment id + file version). * @param string $message Message. */ private static function logo_failed( $name, $message ) { set_transient( 'devisqr_logo_failed', array( 'name' => $name, 'msg' => $message ), WEEK_IN_SECONDS ); self::logo_warning( $message ); } /** * Current logo warning, if any (refreshed by a logo() call). * * @param bool $reset Recompute (after a settings save). * @return string */ public static function logo_warning_text( $reset = false ) { self::logo( $reset ); $w = get_transient( 'devisqr_logo_warning' ); return is_string( $w ) ? $w : ''; } }