1111/**
1212 * Twitter API v2 client with OAuth 1.0a authentication.
1313 *
14- * @since 3.0.0
14+ * OAuth 1.0a requires four credentials from the Twitter Developer Portal
15+ * (Projects & Apps → Your App → "Keys and Tokens"):
16+ * - Consumer Keys section: API Key ($consumerKey) + API Key Secret ($consumerSecret)
17+ * - Authentication Tokens section: Access Token ($accessToken) + Access Token Secret ($accessTokenSecret)
18+ *
19+ * Note: OAuth 2.0 Client ID / Client Secret are separate and NOT used here.
20+ *
21+ * @since 3.2.1
1522 * @license https://opensource.org/licenses/MIT
1623 */
1724readonly class TwitterClient
2027
2128 public function __construct (
2229 private ClientInterface $ httpClient ,
23- private string $ apiKey ,
24- private string $ apiSecret ,
30+ private string $ consumerKey ,
31+ private string $ consumerSecret ,
2532 private string $ accessToken ,
2633 private string $ accessTokenSecret ,
2734 ) {
2835 }
2936
3037 public function isConfigured (): bool
3138 {
32- return !empty ($ this ->apiKey )
33- && !empty ($ this ->apiSecret )
39+ return !empty ($ this ->consumerKey )
40+ && !empty ($ this ->consumerSecret )
3441 && !empty ($ this ->accessToken )
3542 && !empty ($ this ->accessTokenSecret );
3643 }
@@ -91,21 +98,24 @@ private function uploadSingleMedia(AttachmentInterface $attachment): ?string
9198 if (!file_exists ($ filePath )) {
9299 throw new ProviderException ("File not found: {$ filePath }" );
93100 }
94- $ fileContent = file_get_contents ($ filePath );
95- } else {
96- $ fileContent = file_get_contents ($ filePath );
97101 }
98102
99- if ($ fileContent === false ) {
100- throw new ProviderException ("Failed to read file: {$ filePath }" );
103+ // Must pass a resource (not string) so Symfony HttpClient uses multipart/form-data
104+ $ fileHandle = fopen ($ filePath , 'rb ' );
105+ if ($ fileHandle === false ) {
106+ throw new ProviderException ("Failed to open file: {$ filePath }" );
101107 }
102108
103109 $ headers = $ this ->getAuthHeaders ('POST ' , $ url );
104110
105- $ response = $ this ->httpClient ->postMultipart ($ url , $ headers , [
106- 'media ' => $ fileContent ,
107- 'media_category ' => 'tweet_image ' ,
108- ]);
111+ try {
112+ $ response = $ this ->httpClient ->postMultipart ($ url , $ headers , [
113+ 'media ' => $ fileHandle ,
114+ 'media_category ' => 'tweet_image ' ,
115+ ]);
116+ } finally {
117+ fclose ($ fileHandle );
118+ }
109119
110120 if (!$ response ->isSuccessful ()) {
111121 throw new ProviderException ("Failed to upload media: {$ response ->getBody ()}" );
@@ -125,23 +135,24 @@ private function uploadSingleMedia(AttachmentInterface $attachment): ?string
125135 private function getAuthHeaders (string $ method , string $ url , array $ data = []): array
126136 {
127137 $ oauthParams = [
128- 'oauth_consumer_key ' => $ this ->apiKey ,
138+ 'oauth_consumer_key ' => $ this ->consumerKey ,
129139 'oauth_nonce ' => bin2hex (random_bytes (16 )),
130140 'oauth_signature_method ' => 'HMAC-SHA1 ' ,
131141 'oauth_timestamp ' => (string ) time (),
132142 'oauth_token ' => $ this ->accessToken ,
133143 'oauth_version ' => '1.0 ' ,
134144 ];
135145
136- // Create signature base string
146+ // Create signature base string.
147+ // JSON POST body is NOT included per OAuth 1.0a spec (only form-urlencoded bodies are).
137148 $ params = array_merge ($ oauthParams , $ method === 'GET ' ? $ data : []);
138149 ksort ($ params );
139-
150+
140151 $ paramString = http_build_query ($ params , '' , '& ' , PHP_QUERY_RFC3986 );
141152 $ baseString = strtoupper ($ method ) . '& ' . rawurlencode ($ url ) . '& ' . rawurlencode ($ paramString );
142153
143- // Generate signature
144- $ signingKey = rawurlencode ($ this ->apiSecret ) . '& ' . rawurlencode ($ this ->accessTokenSecret );
154+ // Signing key = percent-encode(consumerSecret) + '&' + percent-encode(accessTokenSecret)
155+ $ signingKey = rawurlencode ($ this ->consumerSecret ) . '& ' . rawurlencode ($ this ->accessTokenSecret );
145156 $ signature = base64_encode (hash_hmac ('sha1 ' , $ baseString , $ signingKey , true ));
146157
147158 $ oauthParams ['oauth_signature ' ] = $ signature ;
0 commit comments