&1; echo "__aiclrc:$?"', escapeshellarg( self::exiftool() ), escapeshellarg( $uri ), escapeshellarg( $path ) ); $out = (string) @shell_exec( $cmd ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec, WordPress.WP.AlternativeFunctions.shell_exec_shell_exec -- exiftool invocation; all args escapeshellarg-escaped. return preg_match( '/__aiclrc:0\s*$/', $out ) ? 'ok' : 'failed'; } $packet = self::xmp_packet( $uri ); if ( 'jpg' === $ext || 'jpeg' === $ext ) { return self::jpeg_xmp( $path, $packet ) ? 'ok' : 'failed'; } if ( 'png' === $ext ) { return self::png_xmp( $path, $packet ) ? 'ok' : 'failed'; } return 'unsupported'; } /** * Builds the XMP packet carrying the single IPTC Extension property. * * @since 1.0.0 * * @param string $uri The IPTC Digital Source Type URI to embed. * @return string The serialized XMP packet. */ public static function xmp_packet( $uri ) { return '' . '' . '' . '' . '' . ''; } /* ---------------------------------------------------------------------- */ /* exiftool detection */ /* ---------------------------------------------------------------------- */ /** * Detects the exiftool binary path, caching the result for the request. * * @since 1.0.0 * * @return string|false Path to exiftool, or false if unavailable. */ public static function exiftool() { static $cached = null; if ( null !== $cached ) { return $cached; } $cached = false; if ( ! function_exists( 'shell_exec' ) ) { return $cached; } $disabled = explode( ',', (string) ini_get( 'disable_functions' ) ); if ( in_array( 'shell_exec', array_map( 'trim', $disabled ), true ) ) { return $cached; } $which = @shell_exec( 'command -v exiftool 2>/dev/null' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec, WordPress.WP.AlternativeFunctions.shell_exec_shell_exec -- exiftool invocation; all args escapeshellarg-escaped. if ( is_string( $which ) && '' !== trim( $which ) ) { $cached = trim( $which ); } return $cached; } /** * Determines whether a path resolves inside the uploads directory. * * @since 1.0.0 * * @param string $path Absolute path to test. * @return bool True only when $path resolves inside the uploads directory. */ protected static function within_uploads( $path ) { $base = wp_get_upload_dir(); if ( empty( $base['basedir'] ) ) { return false; } $real = realpath( $path ); $root = realpath( $base['basedir'] ); return $real && $root && 0 === strpos( $real, $root ); } /* ---------------------------------------------------------------------- */ /* Pure-PHP JPEG (APP1) XMP */ /* ---------------------------------------------------------------------- */ /** * Injects an XMP packet into a JPEG file as an APP1 segment. * * @since 1.0.0 * * @param string $path Absolute path to the JPEG file. * @param string $packet The serialized XMP packet to inject. * @return bool True on success, false on failure or malformed input. */ protected static function jpeg_xmp( $path, $packet ) { $data = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Binary read for in-place XMP injection; WP_Filesystem returns no raw bytes. if ( false === $data || "\xFF\xD8" !== substr( $data, 0, 2 ) ) { return false; } $ns = "http://ns.adobe.com/xap/1.0/\x00"; $body = $ns . $packet; $seglen = strlen( $body ) + 2; if ( $seglen > 0xFFFF ) { return false; } $xmp_seg = "\xFF\xE1" . pack( 'n', $seglen ) . $body; $out = "\xFF\xD8"; $pos = 2; $len = strlen( $data ); $inserted = false; while ( $pos + 1 < $len ) { if ( "\xFF" !== $data[ $pos ] ) { return false; } $marker = ord( $data[ $pos + 1 ] ); if ( 0xFF === $marker ) { $pos++; continue; } if ( 0xDA === $marker ) { if ( ! $inserted ) { $out .= $xmp_seg; $inserted = true; } $out .= substr( $data, $pos ); return self::put( $path, $out ); } if ( 0x01 === $marker || ( $marker >= 0xD0 && $marker <= 0xD9 ) ) { $out .= substr( $data, $pos, 2 ); $pos += 2; continue; } if ( $pos + 4 > $len ) { return false; } $unpacked = unpack( 'n', substr( $data, $pos + 2, 2 ) ); if ( false === $unpacked ) { return false; } $slen = $unpacked[1]; $segment = substr( $data, $pos, 2 + $slen ); $is_xmp = ( 0xE1 === $marker && 0 === strpos( substr( $segment, 4 ), 'http://ns.adobe.com/xap/1.0/' ) ); $is_appn = ( $marker >= 0xE0 && $marker <= 0xEF ); if ( ! $inserted && ! $is_appn ) { $out .= $xmp_seg; $inserted = true; } if ( ! $is_xmp ) { $out .= $segment; } $pos += 2 + $slen; } return false; } /* ---------------------------------------------------------------------- */ /* Pure-PHP PNG (iTXt) XMP */ /* ---------------------------------------------------------------------- */ /** * Injects an XMP packet into a PNG file as an iTXt chunk. * * @since 1.0.0 * * @param string $path Absolute path to the PNG file. * @param string $packet The serialized XMP packet to inject. * @return bool True on success, false on failure or malformed input. */ protected static function png_xmp( $path, $packet ) { $data = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Binary read for in-place XMP injection; WP_Filesystem returns no raw bytes. $sig = "\x89PNG\r\n\x1a\n"; if ( false === $data || substr( $data, 0, 8 ) !== $sig ) { return false; } $itxt_data = "XML:com.adobe.xmp\x00" // keyword . "\x00" // compression flag (uncompressed) . "\x00" // compression method . "\x00" // language tag (empty) . "\x00" // translated keyword (empty) . $packet; // UTF-8 XMP $chunk = pack( 'N', strlen( $itxt_data ) ) . 'iTXt' . $itxt_data . pack( 'N', crc32( 'iTXt' . $itxt_data ) ); $out = $sig; $pos = 8; $len = strlen( $data ); while ( $pos + 8 <= $len ) { $clen = unpack( 'N', substr( $data, $pos, 4 ) )[1]; $type = substr( $data, $pos + 4, 4 ); $full = substr( $data, $pos, 12 + $clen ); if ( 'IEND' === $type ) { $out .= $chunk . $full; $out .= substr( $data, $pos + 12 + $clen ); return self::put( $path, $out ); } $is_xmp = ( 'iTXt' === $type && 0 === strpos( substr( $data, $pos + 8, 18 ), 'XML:com.adobe.xmp' ) ); if ( ! $is_xmp ) { $out .= $full; } $pos += 12 + $clen; } return false; } /** * Atomically writes new file contents via a temp file and rename. * * @since 1.0.0 * * @param string $path Absolute path to the destination file. * @param string $contents The full binary file contents to write. * @return bool True on success, false on failure. */ protected static function put( $path, $contents ) { $tmp = $path . '.aicl-tmp'; if ( false === file_put_contents( $tmp, $contents ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents -- Binary write for in-place XMP injection; WP_Filesystem returns no raw bytes. return false; } if ( ! @rename( $tmp, $path ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.rename_rename -- Atomic in-place replace of a binary image file within uploads; @ suppresses the failure warning, handled just below. wp_delete_file( $tmp ); return false; } return true; } }