Skip to content

Instantly share code, notes, and snippets.

@katsube
Last active December 29, 2023 04:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save katsube/b0876c77cf91b8bbac21cbc5df41d691 to your computer and use it in GitHub Desktop.
Save katsube/b0876c77cf91b8bbac21cbc5df41d691 to your computer and use it in GitHub Desktop.
[PHP] はてなブログに投稿するクラス
<?php
/**
* はてなブログに投稿するクラス
*
* @package HatenaBlogPost
* @version 0.1.0
* @see https://developer.hatena.ne.jp/ja/documents/blog/apis/atom/
* @see https://developer.hatena.ne.jp/ja/documents/auth/apis/wsse
* @example
* require_once('HatenaBlogPost.php');
*
* // ユーザー情報
* $user_id = 'your hatena id (foo)';
* $api_key = 'your api key (xxxxxxxxxx)';
* $blog_id = 'your blog id (xxxx.hatenablog.com)';
*
* // 投稿する
* $hatena = new HatenaBlogPost($user_id, $api_key, $blog_id);
* $hatena->post('記事のタイトル', '記事の本文'); // 本文中のHTMLは文字参照などに変換しないと消えます
*
* // オプションを指定する場合
* $haten->post('記事のタイトル', '記事の本文', [
* 'category' => 'カテゴリ1,カテゴリ2', // カテゴリ
* 'draft' => false, // 下書きかどうか
* 'updated' => '2023-12-31T00:00:00Z', // 投稿日時
* 'url' => 'foobar', // カスタムURL(パスの部分。先頭にスラッシュは付けない)
* 'escapehtml' => true, // 本文のHTMLをエスケープするかどうか
* ]);
*/
class HatenaBlogPost{
//---------------------------------------------
// プロパティ
//---------------------------------------------
private $user_id; // はてなブログのユーザーID
private $api_key; // はてなブログのAPIキー
private $blog_id; // はてなブログのブログID
// はてなブログのエンドポイント
private $endpoint = 'https://blog.hatena.ne.jp/{user_id}/{blog_id}/atom/entry';
/**
* コンストラクタ
*
* @param string $user_id はてなブログのユーザーID
* @param string $api_key はてなブログのAPIキー
* @param string $blog_id はてなブログのブログID
*/
function __construct($user_id, $api_key, $blog_id){
$this->user_id = $user_id;
$this->api_key = $api_key;
$this->blog_id = $blog_id;
}
/**
* 投稿する
*
* @param string $title 記事のタイトル
* @param string $body 記事の本文 ※HTMLは文字参照などに変換しないと消えます
* @param array [$options=null] オプション (category, draft, updated, url, escapehtml)
* @return bool
*/
function post($title, $body, $options=null){
$xml = $this->createXml($title, $body, $options);
$response = $this->request($xml);
$status_code = $response['status_code'];
$body = $response['body'];
if( $status_code === 201 ){
return true;
}
else{
$message = sprintf('[%s]Post Error: %s', $status_code, $body);
throw new Exception($message);
}
}
/**
* エンドポイントを設定
*
* @param string $endpoint
* @return void
*/
function setEndpoint($endpoint){
$this->endpoint = $endpoint;
}
/**
* リクエストを送信
*
* @param string $xml
* @return array
* @access private
*/
private function request($xml){
$endpoint = $this->getEndpoint();
$headers = [
'Content-Type: application/x.atom+xml',
'X-WSSE: ' . $this->getWsseHeader(),
];
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $xml);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($curl);
$status_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
$body = substr($response, strpos($response, "\r\n\r\n") + 4);
return [
'status_code' => $status_code,
'body' => $body,
];
}
/**
* XMLを生成
*
* @param string $title
* @param string $body
* @param array [$options=null]
* @return string
* @access private
*/
private function createXml($title, $body, $options=null){
$category = isset($options['category']) ? $options['category'] : null; // カテゴリ
$draft = isset($options['draft']) ? $options['draft'] : false; // 下書きかどうか
$updated = isset($options['updated']) ? $options['updated'] : null; // 投稿日時
$url = isset($options['url']) ? $options['url'] : null; // カスタムURL
$escape = isset($options['escapehtml']) ? $options['escapehtml'] : false; // 本文のHTMLをエスケープするかどうか
$xmlbase =<<<EOX
<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app">
</entry>
EOX;
$xml = new SimpleXMLElement($xmlbase);
// タイトル
$xml->addChild('title', $title);
$xml->addChild('author')->addChild('name', $this->user_id);
// 本文
if( $escape ){
$body = htmlspecialchars($body, ENT_QUOTES, 'UTF-8');
}
$xml->addChild('content', $body)->addAttribute('type', 'text/plain');
// 更新日時
if( $updated !== null ){
$xml->addChild('updated', $updated);
}
// カテゴリー
if( $category !== null){
$categories = explode(',', $category);
foreach( $categories as $category ){
$xml->addChild('category')->addAttribute('term', $category);
}
}
// 下書き?
if( $draft ){
$xml->addChild('app:control')->addChild('app:draft', 'yes', 'http://www.w3.org/2007/app');
}
// カスタムURL
if( $url !== null ){
$custom_url = $xml->addChild('hatenablog:custom-url', $url, 'http://www.hatena.ne.jp/info/xmlns#hatenablog');
$custom_url->addAttribute('xmlns:hatenablog', 'http://www.hatena.ne.jp/info/xmlns#hatenablog');
}
// 文字列にして返却する
return $xml->asXML();
}
/**
* エンドポイントを取得
*
* @return string
* @access private
*/
private function getEndpoint(){
$endpoint = str_replace('{user_id}', $this->user_id, $this->endpoint);
$endpoint = str_replace('{blog_id}', $this->blog_id, $endpoint);
return $endpoint;
}
/**
* X-WSSEヘッダーを作成
*
* @return string
* @access private
*/
private function getWsseHeader(){
$nonce = sha1(time() . uniqid() . rand());
$created = date('Y-m-d\TH:i:s\Z');
$password_digest = base64_encode(sha1($nonce . $created . $this->api_key, true));
$wsse_header = sprintf('UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"',
$this->user_id,
$password_digest,
$nonce,
$created
);
return $wsse_header;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment