【PHP】ファイルサーバーをPHPで作ってみた

こんにちは、技術猫です。
(初投稿です)

ApacheとPHP(とTailwind CSS)でファイルサーバーを作ってみました。
この記事では、ログイン機能に必要なJSONファイルの読み取りの方法や、
ファイルのアップロード機能の実装方法などを紹介します。
そしておまけで、PHPでのコマンドの実行方法などを紹介します。

:::note info
コード内のコメントアウトされてる部分を読むと意味がわかるよ！
:::

:::note alert
この記事で紹介するコードの一部は、セキュリティが万全ではない可能性があります。
社外などに公開することは推奨しません！
:::

## ソース

https://github.com/DiamondGotCat/tcloud

作成した全部のソースコードはGitHubのreleaseにてMITライセンスで配布しています。

一応ApacheとPHPがあれば使えます。(テスト環境はmacOS)

## ログイン機能
ログイン機能は、最初はパスワードハッシュをPHP内で書いていましたが、
後から出てくる「ユーザー追加機能」という機能が必要なため、
Jsonファイルに保存することになりましたが...

### 早速間違えた！
プログラミング初心者なので、JSONファイルをPHPで読み込む際に、変な書き方をしていました。

```php:users.json

{"users":[
    {
        "username":"admin",
        "password":"$2y$10$3dmC2Lq8r/9B8F83NrLqMulwkwuk.QgJsvQXXumwX83k6mQzmjQP2"
    }
]}

```

```php:login.php (修正前)

<?php
    require_once __DIR__ . '/functions.php';
    require_unlogined_session();
    function get_password($username) {
        $json = file_get_contents('http://192.168.1.100/tCloud/users.json');
        $datas = json_decode($json, true);
        $name = $datas['users']
        return $name[$username]['password']; 
        # users.jsonの中でも、"users"の中にはusernameはなく、"users"の1番目の中にusernameがある。
    }
    $username = filter_input(INPUT_POST, 'username');
    $password = filter_input(INPUT_POST, 'password');
    if ($username == "rsa"){

        header("Location: rsa.php");
        exit;

    }
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {

        if (password_verify($password,get_password($username))){ # なので、ここでエラー。
            session_regenerate_id(true);
            $_SESSION['username'] = $username;
            header('Location: /tCloud/');
            exit;
        } else {
            http_response_code(403);
        }
    }
    header('Content-Type: text/html; charset=UTF-8');
# この先省略
```

Microsoft Copilot(Bing)に教えてもらった修正点を使って修正した後はこちら:

```php:login.php (修正後)

<?php
    require_once __DIR__ . '/functions.php';
    require_unlogined_session();
    function get_password($username) {
        $json = file_get_contents('http://192.168.1.100/tCloud/users.json');
        $datas = json_decode($json, true);
        foreach ($datas['users'] as $name) {
            if ($name['username'] == $username) {
                return $name['password']; #ちゃんと全部読み取ってみて
            }
        }
        return null; # なかったらNull
    }
    $username = filter_input(INPUT_POST, 'username');
    $password = filter_input(INPUT_POST, 'password');
    if ($username == "rsa"){
        header("Location: rsa.php");
        exit;
    }
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if (password_verify($password,get_password($username))){
            session_regenerate_id(true);
            $_SESSION['username'] = $username;
            header('Location: /tCloud/');
            exit;
        } else {
            http_response_code(403);
        }
    }
    header('Content-Type: text/html; charset=UTF-8');

# この先省略
```
という感じです。
何番目かを指定する必要があるので、foreachにする必要があることにこの時気がつきました。

## ストレージのアップロード機能

アップロードは一つずつ調べて完成しました。
ユーザーごとのストレージがあるのと、共有ドライブがある感じです。

```php:upload.php (共有ドライブ)

<?php

require_once __DIR__ . '/../functions.php';
require_logined_session();

header('Content-Type: text/html; charset=UTF-8');

?>

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Share File</title>
        <link rel="stylesheet" href="/dist/output.css" />
    </head>

    <body class="text-black bg-white">
        <div class="flex flex-col h-screen">
        <div class="">
            <font color="blue">
                <a href="#" onclick="history.back()"> < back </a>
            </font>
        </div>
            <div>
                <div class="flex justify-center mt-20">
                    <div class="w-9/12 border rounded-3xl bg-gray-200 dark:bg-gray-200">
                        <div class="my-16 text-center">

                            <img src="/link.png" style="display: block; margin: auto;">
                            <br><br>

                            <h2 class="text-xl">Upload</h2>

                            <font color="orange">

                            <?php
                                $temporary_file = $_FILES['user_file_name']['tmp_name']; # 一時ファイル名
                                $true_file = $_FILES['user_file_name']['name']; # 本来のファイル名

                                # is_uploaded_fileメソッドで、一時的にアップロードされたファイルが本当にアップロード処理されたかの確認
                                if (is_uploaded_file($temporary_file)) {
                                    if (move_uploaded_file($temporary_file , $true_file )) {
                                        echo "Upload completed.
                                        The file is located at the following URL:<br>
                                        <a href='http://192.168.1.100:8080/tCloud/files/" . $true_file . "'>http://192.168.1.100:8080/files/" . $true_file . "</a>";
                                    } else {
                                        echo "Upload failed.";
                                    }
                                } else {
                                    echo "Upload failed.";
                                }
                            ?>

                            </font>

                            <br><br>
                            <form enctype="multipart/form-data"  action="upload.php" method="POST">
                              <input type="hidden" name="name" value="value" />
                              <input name="user_file_name" type="file" />
                              <input type="submit" value="Upload" />
                            </form> 
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </body>
</html>
```

## おまけ: Admin Console

そしておまけ機能として、Admin Consoleというものを作りました。
ユーザーの追加を管理者ができるようなツールです。

```php:tCloud/admin/add.php (Admin Console)

<?php

require_once __DIR__ . '/../functions.php';
require_logined_session();

$username = $_SESSION['username'];

if ($username == "admin"){

} else{
    header("Location: /403.php");
    exit();
}

header('Content-Type: text/html; charset=UTF-8');

?>

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Add User - tCloud</title>
        <link rel="stylesheet" href="/dist/output.css" />
    </head>
    <body class="text-black bg-white">
        <div class="flex flex-col h-screen">
        <div class="">
            <br>
        </div>
            <div>
                <div class="flex justify-center mt-20">
                    <div class="text-center">

                        <img src="/icon-128x128.png" style="display: block; margin: auto;">
                        <h1 class="text-5xl">tCloud</h1>
                        Admin Console
                        <br><br>
                        <form action="adduser.php" method="POST" class="mt-12 disabled:bg-slate-50 disabled:text-slate-500  disabled:shadow-none  invalid:text-pink-600  focus:invalid:ring-pink-500">
                            <div class="mb-3">
                                <input
                                    type="text"
                                    placeholder="ID"
                                    name="username"
                                    required
                                    class="w-80 focus:ring-2 focus:ring-blue-500 focus:outline-none appearance-none text-sm text-slate-900 placeholder-slate-400 rounded-md py-2 ring-1 ring-slate-200 shadow-sm"
                                />
                            </div>
                            <div class="mb-5">
                                <input
                                    type="text"
                                    placeholder=" PW"
                                    name="password"
                                    minlength="6"
                                    class="w-80 focus:ring-2 focus:ring-blue-500 focus:outline-none appearance-none text-sm text-slate-900 placeholder-slate-400 rounded-md py-2 ring-1 ring-slate-200 shadow-sm"
                                />
                            </div>
                            <input type="hidden" name="token" value="<?=h(generate_token())?>">
                            <button type="submit">
                                Create User
                            </button>
                        </form>

                    </div>
                </div>
            </div>
        </div>
    </body>
</html>

```

処理側は二つあって、

/tCloud/admin/adduser.phpと、
/tCloud/drive/adduser.phpがあります。

やっていることはコマンド実行とファイル作成です。



adminフォルダのadduser.phpは、

```php:tCloud/admin/adduser.php (Admin Console)

<?php

    require_once __DIR__ . '/../functions.php';
    require_logined_session();

    $username = $_SESSION['username'];

    if ($username == "admin"){

    } else{
        header("Location: /403.php");
        exit();
    }

    $user = [
        'username' => $_POST['username'],
        'password' => password_hash($_POST['password'], PASSWORD_BCRYPT)
    ];

    $json = file_get_contents('http://192.168.1.100/tCloud/users.json'); # jsonを取得
    $data = json_decode($json, true); # jsonデータを配列へ
    $data['users'][] = $user; # jsonファイルの中の'users'の中を指定
    $json = json_encode($data); # 配列をjsonデータへ
    file_put_contents('../users.json', $json); # 'users'に追記

    header("Location: ../drive/adduser.php?username=" . $_POST['username']);
    # driveのadduser.phpに移動

?>

```

という感じです。
driveのadduser.phpに移動したら、以下のファイルが読み込まれます。

```php:tCloud/admin/adduser.php (Admin Console)

<?php

    require_once __DIR__ . '/../functions.php';
    require_logined_session();

    $username = $_SESSION['username'];

    if ($username == "admin"){

    } else{
        header("Location: /403.php");
        exit();
    }

    exec("mkdir " . $_GET['username']); # ディレクトリ(フォルダ)作成
    exec("chmod 777 " . $_GET['username']); # 読み込み・書き込み権限つける
    exec("touch " . $_GET['username'] . "/index.php"); # index.php作成
    exec("chmod 777 " . $_GET['username'] . "/index.php"); # 読み込み・書き込み権限つける
    $file = fopen($_GET['username'] . "/index.php", "w");# ファイルを開く
    fwrite($file, '
    
    <?php

    require_once __DIR__ . "/../../functions.php";
    require_logined_session();
    
    $username = $_SESSION["username"];
    
    if ($username == "' . $_GET['username'] . '"){
    
    } else{
        header("Location: /403.php");
        exit();
    }
    
    header("Content-Type: text/html; charset=UTF-8");
    
    ?>
    
    <!DOCTYPE html>
    <html lang="ja">
        <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>Your Drive</title>
            <link rel="stylesheet" href="/dist/output.css" />
        </head>
    
        <body class="text-black bg-white">
            <div class="flex flex-col h-screen">
            <div class="">
                <font color="blue">
                    <a href="#" onclick="history.back()"> < back </a>
                </font>
            </div>
                <div>
                    <div class="flex justify-center mt-20">
                        <div class="w-9/12 border rounded-3xl bg-gray-200 dark:bg-gray-200">
                            <div class="my-16 text-center">
    
                              
                                <img src="/drive.png" style="display: block; margin: auto;">
                                <br><br>
    
                                <h2 class="text-xl">Upload</h2>
    
                                <form enctype="multipart/form-data"  action="upload.php" method="POST">
                                  <input type="hidden" name="name" value="value" />
                                  <input name="user_file_name" type="file" />
                                  <input type="submit" value="Upload" />
                                </form> 
    
                                <br><br>
    
                                <h2 class="text-xl">Files</h2>
    
                                <?PHP
                                  $dirpath = "./";
                                  $dirlist = dir($dirpath);
                                  while( $filename = $dirlist->read() ){
                                    if( (is_dir($filename) == false) && ($filename!=".." || $filename!= "." ) ){
                                      if ($filename !== "index.php" && $filename !== "upload.php"){
                                        print("<a href=\"/download.php?author=chii&path=" .  $filename . "\"><button>".$filename."</button></a><br>\n");
                                      }
                                    }
                                  }
                                  $dirlist->close();
                                ?>
                                
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </body>
    </html>
    
    
    
    '); # ファイルに書き込み
    fclose($file); # ファイルを閉じる

    exec("touch " . $_GET['username'] . "/upload.php"); # upload.phpを作成
    exec("chmod 777 " . $_GET['username'] . "/upload.php"); # 読み込み・書き込み権限をつける
    $file = fopen($_GET['username'] . "/upload.php", "w"); # upload.phpを開く
    fwrite($file, '
    
    <?php

require_once __DIR__ . "/../../functions.php";
require_logined_session();

header("Content-Type: text/html; charset=UTF-8");

$username = $_SESSION["username"];

if ($username == "' . $_GET['username'] . '"){

} else{
    header("Location: /403.php");
    exit();
}

?>

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Your Drive</title>
        <link rel="stylesheet" href="/dist/output.css" />
    </head>

    <body class="text-black bg-white">
        <div class="flex flex-col h-screen">
        <div class="">
            <font color="blue">
                <a href="#" onclick="history.back()"> < back </a>
            </font>
        </div>
            <div>
                <div class="flex justify-center mt-20">
                    <div class="w-9/12 border rounded-3xl bg-gray-200 dark:bg-gray-200">
                        <div class="my-16 text-center">

                          
                            <img src="/drive.png" style="display: block; margin: auto;">
                            <br><br>

                            <h2 class="text-xl">Upload</h2>

                            <font color="orange">

                            <?php
                                $temporary_file = $_FILES["user_file_name"]["tmp_name"]; 
                                $true_file = $_FILES["user_file_name"]["name"]; 

                                if (is_uploaded_file($temporary_file)) {
                                    if (move_uploaded_file($temporary_file , $true_file )) {
                                        echo $true_file . " is Uploaded";
                                    } else {
                                        echo "Upload failed";
                                    }
                                } else {
                                    echo "Upload failed";
                                }
                                var_dump($true_file);
                            ?>

                            </font>

                            <br><br>
                            <form enctype="multipart/form-data"  action="upload.php" method="POST">
                              <input type="hidden" name="name" value="value" />
                              <input name="user_file_name" type="file" />
                              <input type="submit" value="Upload" />
                            </form> 

                            <br><br>

                            <h2 class="text-xl">Files</h2>

                            <?PHP
                              $dirpath = "./";	
                              $dirlist = dir($dirpath);
                              while( $filename = $dirlist->read() ){
                                if( (is_dir($filename) == false) && ($filename!=".." || $filename!= "." ) ){
                                  if ($filename !== "index.php" && $filename !== "upload.php"){
                                    print("<a href=\"/download.php?author=chii&path=" .  $filename . "\"><button>".$filename."</button></a><br>\n");
                                  }
                                }
                              }
                              $dirlist->close();
                            ?>
                            
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </body>
</html>


    
    '); # ファイルに書き込み
    fclose($file); # ファイルを閉じる

    header("Location: ../admin/index.php"); # 最初の画面に戻る

?>

```

ちょっと作るの難しかったです...

## 終わりに

どうだったでしょうか。
初投稿なのでまだ書き方など難しいですが、
JSONの読み取り方法など参考になれば幸いです。

https://github.com/DiamondGotCat/tcloud

GitHubのreleaseでも公開しているのでぜひみてみてください。
見てくれてありがとうございました。
それではまた他の記事で！