|||||||||||||||||||||

なんぶ電子

- 更新: 

PHPとJavaScriptでエラーチェックを同期

JavaScript

PHPでWebアプリを作っている時に面倒くさいのが、サーバーサイド(PHP)と、ブラウザサイド(JavaScript)のエラーチェックです。

サーバーサイドだけしてしまえばユーザーの使い勝手が悪くなり、ブラウザサイドだけにしてしまうと思わぬデータを受け取ってしまったりします。といって両方に定義すると今度は、修正時の同期が面倒になります。

そこで筆者は、エラーチェックのルールをJSONファイル化し、PHPとJavascript両者から同じファイルを参照させることで同期を図りました。

エラーチェックの基本ルール

チェックする項目は連想配列の状態になっていて、値は文字列または数値になっているものとします。

$values['name']="徳川家康"
$values['period']="江戸"
$values['start']=1603

チェック内容を次のように大別します。

  • 正規表現チェック

    正規表現に一致した場合のみ許可します。

  • ホワイトリストチェック

    利用できる値のリストを定義します。

  • ブラックリストチェック

    利用できない値のリストを定義します。

  • 比較チェック

    等号や不等号によるチェックです。

  • nullチェック

    NULLやundefinedだった場合にエラーとします。

  • 全空白チェック(大小混合)

    大小を含んだスペースを除去した際に文字長さが0だった場合にエラーとします。

  • 関数チェック

    上記のチェックで対応できない場合に、関数によるチェックを行います。

JSONファイル

ルールを記述するJSONファイルは、正規のJSONファイルでもいいのですが、ファイル処理が面倒なJavaScript側での処理が簡単になるようにjsファイルとして作成し、PHP側はプログラムの処理でJSONの部分だけを取得するようにします。

validateparam.js

const getValidateParam = function() {return `
[
{ "target":"name", "type": "notblank","msg":"名前を空白にすることはできません", "require" : 1 },
{ "target":"name", "type": "regex", "value": "^.{1,20}$","msg":"名前は1~20桁で設定してください" },
{ "target":"gender", "type": "allow","value":["女","男","未回答"],"msg":"性別を指定して下さい","require" : 1},
{ "target": "age", "type": "regex", "value": "[0-9]{1,3}","msg":"1~3桁で設定してください","require" : 1 },
{ "target": "age", "type": "comp","value":[[0,"<="],["$answer1",">"]],"msg":"年齢が不正です" },
{ "target": "memo", "type": "function","value":{"php":"checkfunc", "js":"checkfunc"},"msg":"URLは記述できません"},
{ "target": "answer1","condor":[
{ "target": "answer2","type": "allow","value": [1] },
{ "target": "answer3","type": "allow","value": [1] }
],
"type": "allow","value":[100],"msg":"answer1には100をセットしてください" }
]
`;}

JSONの項目について説明していきます。

  • target

    どの項目をチェックするかです。連想配列のキーに一致させます。

  • type

    前述で大別したチェックの種類を設定します。値は次の通りです。

    • 正規表現チェック:regex

      valueには正規表現の式を入れます。

    • ホワイトリストチェック:allow

      ホワイトリストは、配列の形でvalueをセットします。

    • ブラックリストチェック:deny

      ブラックリストは、配列の形でvalueをセットします。

    • 比較チェック:comp

      比較チェックは[値,"等号または不等号"]の2値で構成される配列を、さらに配列値としてセットします。不等号の場合はチェックしたい値が右側にくると仮定して向きを決めます。値をほかの入力値と比べたい場合は、比較したいキー名称の頭に$をつけ、文字列として値の位置にセットします。

    • nullチェック:notnull

      nullチェックではvalueの項目は使いません。

    • 全空白チェック:notblank

      全空白チェックではvalueの項目は使いません。

    • 関数チェック:function

      関数チェックではvalueをオブジェクトで設定します。PHPで処理する際の関数名を指定する「php」とJavascriptで処理する際の「js」の項目を設定します。

  • value

    チェックの種類に応じて値を設定します。

  • msg

    エラー時のメッセージを設定します。

  • require

    targetに設定したキーの値が、存在しなかった場合にエラーとします。

  • condor

    チェックを行うときのサブ条件です。このサブ条件のチェックを通過しなければ、メインのチェックをしません。

    condorは複数のサブ条件をorでつなげます。いずれかのサブ条件が満たされた時点でサブチェックは終了します。

    ここで設定する条件の記述方法は、メインの条件と同じです。ただしサブ条件ではcondorは使えません。

  • condand

    複数の条件をandでつなげること以外condorと一緒です。

JSON内では主に文字列を記述することになりますが、文字列中でエスケープが必要なものは、"¥/(ダブルクォーテーション、バックスラッシュ・円記号、スラッシュ)です。エスケープ文字は¥です。

PHPは次のようにクラスをコーディングしました。

validate.php

<?php
/* PHP validateクラス */

//使い方 
/*
$v = new Validate();
$rules=$v->openValidationFile('./validateparam.js');
if($rules===NULL) {
  echo "validationの読み込みに失敗しました";
  return;
}
//条件表示(debug)
var_dump($rules);

if($v->check($rules,$v->makeSampleData())===FALSE) {
  var_dump($v->getError());
}
*/

class Validate {  
  private $mErrMsgs=array();
  private $mErrObjs=array();
  
  function clearError() {
    $this->mErrMsgs = array();//エラー初期化
    $this->mErrObjs = array();//エラー初期化
  }
  
  function openValidationFile($strFilePath) {
    $blnFirst = FALSE;
    $strJson="";
    $fp = @fopen($strFilePath,'r');
    
    if ($fp===FALSE) {
      return FALSE;
    }
    
    while(!feof($fp)) {
      $strLine = rtrim(fgets($fp));
      if ($strLine==='`;}'){
        break;
      }
      if ($blnFirst) {
        $strJson .= $strLine;
      } else {
        $blnFirst = TRUE;
      }
    }
    fclose($fp);
    
    $array = json_decode($strJson,true);
    
    if ($array===NULL || $array === TRUE || $array === FALSE) {
      return NULL;
    } else {
      return $array;
    }
  }

  function check($validations, $checkValues) {

    $this->clearError();
    $blnValid = TRUE;
    
    //PHPの連想配列は入力順が保持される前提
    foreach($validations as $validationKey => $validationValue) {
      $blnExists = FALSE;

      //次の値が存在しなかったらチェックしようがない
      if (isset($validationValue['target'])=== FALSE) {
        $this->mErrMsgs[]="targetの記述がありません。行:".(string)$validationKey;
        $blnValid = FALSE;
        continue;
      }
      if (isset($validationValue['type'])=== FALSE)  {
        $this->mErrMsgs[]="typeの記述がありません。行:".(string)$validationKey;
        $blnValid = FALSE;
        continue;
      }

      foreach($checkValues as $checkValueKey => $checkValueValue) {
        if ($checkValueKey!==$validationValue['target']) continue;

        $blnExists = TRUE;

        //condandチェック
        if (isset($validationValue['condand'])) {
          $blnContinue = FALSE;

          if (is_array($validationValue['condand'])) {
            for ($i = 0; $i < count($validationValue['condand']);$i++) {
              if (isset($validationValue['condand'][$i]['target'])=== FALSE) {
                $this->mErrMsgs[]="condandのtargetの記述がありません。行:".(string)$validationKey."-".(string)$i;
                $blnValid = FALSE;
                continue;
              }
              if (isset($validationValue['condand'][$i]['type'])=== FALSE)  {
                $this->mErrMsgs[]="condandのtypeの記述がありません。行:".(string)$validationKey."-".(string)$i;
                $blnValid = FALSE;
                continue;
              }
             
              if (isset($checkValues[$validationValue['condand'][$i]['target']])) {
                if ($this->checkSub($validationValue['condand'][$i],$checkValues[$validationValue['condand'][$i]['target']],$checkValues)===FALSE) {
                  $blnContinue = TRUE;
                  break;
                }
              } else {
                $this->mErrMsgs[]="condandの複合条件の参照先が存在しません。".$validationValue['condand'][$i]['target']."行:".(string)$validationKey."-".(string)$i;
                $blnContinue = TRUE;
                $blnValid = FALSE;
                break;
              } 
            }
            if ($blnContinue) {
              continue;
            }
          } else {
            $this->mErrMsgs[]=$validationValue['target']."のcondand設定が不正です";
            $blnValid = FALSE;  
          }

        }
        //condorチェック
        if (isset($validationValue['condor'])) {
          $blnContinue = TRUE;
          if (is_array($validationValue['condor'])) {
            for ($i = 0; $i < count($validationValue['condor']);$i++) {
              if (isset($validationValue['condor'][$i]['target'])=== FALSE) {
                $this->mErrMsgs[]="condorのtargetの記述がありません。行:".(string)$validationKey."-".(string)$i;
                $blnValid = FALSE;
                continue;
              }
              if (isset($validationValue['condor'][$i]['type'])=== FALSE)  {
                $this->mErrMsgs[]="condorのtypeの記述がありません。行:".(string)$validationKey."-".(string)$i;
                $blnValid = FALSE;
                continue;
              }
              if (isset($checkValues[$validationValue['condor'][$i]['target']])) {
            
                if ($this->checkSub($validationValue['condor'][$i],$checkValues[$validationValue['condor'][$i]['target']],$checkValues)===TRUE) {
                  $blnContinue = FALSE;
                  break;
                }
              } else {
                $this->mErrMsgs[]="condorの複合条件の参照先が存在しません。行:".(string)$validationKey."-".(string)$i;
                $blnValid = FALSE;
                $blnContinue = TRUE;
                break;
              }
            }
            if ($blnContinue) {
              continue;
            }
          } else {
            $this->mErrMsgs[]=$validationValue['target']."のcondor設定が不正です";
            $blnValid = FALSE;  
          }
        }
        
        
        if ($this->checkSub($validationValue,$checkValueValue,$checkValues) === FALSE) {
          if (isset($validationValue['msg'])) {
            $this->mErrMsgs[]=$validationValue['msg'];
          } else {
            $this->mErrMsgs[]="エラー(".$validationValue['target'].")";
          }
          $this->mErrObjs[]=$validationValue['target'];

          $blnValid = FALSE;
        }
        
      }

      if (isset($validationValue['require']) && $validationValue['require']==1 && !$blnExists) {
        $this->mErrMsgs[]=$validationValue['target']."の項目がデータに存在しません";
        $blnValid = FALSE;  
      }
      
    }

    return $blnValid;
  }

  private function checkSub($validation, $checkValue, &$checkValues) {
    $blnResult = TRUE;
    
    switch($validation['type']) {
    case "notblank":
      //"noblank";
      if(strlen(preg_replace("/( | )/", "", $checkValue )) <= 0) {
        $blnResult = FALSE;
      }
      break;
    case "regex":
      //正規表現(mb)
      if (!isset($validation['value'])) {
      $blnResult = FALSE;
        break;
      }
      if (mb_ereg_match($validation['value'],$checkValue)!=1) {
        $blnResult = FALSE;
      }
      break;
    case "comp":
      //比較
      if (!isset($validation['value'])) {
        $blnResult = FALSE;
        break;
      }
      if (is_array($validation['value'])) {
        for ($i = 0; $i < count($validation['value']); $i++) {
          if ($this->compare($validation['value'][$i],$checkValue,$checkValues)===FALSE) {
            $blnResult = FALSE;
            break;
          }
        }
      }
      break;
    case "deny":
      if (!isset($validation['value'])) {
        $blnResult = FALSE;
        break;
      }
      if (is_array($validation['value'])) {
        for ($i = 0; $i < count($validation['value']); $i++) {
          if ($validation['value'][$i]===$checkValue) {
            $blnResult = FALSE;
            break;
          }
        }
      }
      break;
    case "allow":
      if (!isset($validation['value'])) {
        $blnResult = FALSE;
        break;
      }
      if (is_array($validation['value'])) {
        for ($i = 0; $i < count($validation['value']); $i++) {
          if ($validation['value'][$i]===$checkValue) {
            //一致
            break 2;
          }
        }
        $blnResult = FALSE;
      } else {
        $blnResult = FALSE;
      }
      break;
    case "notnull":
      if ($checkValue === NULL) {
        $blnResult = FALSE;
      }
      break;
    case "function":
      if (!isset($validation['value']['php'])) break;//これだけチェック内容がセットされていなかったらTRUE
      
      //evalを使えばJSONに関数を書くこともできますが危険です
      //eval('$func='.$validation['value']['php']);
      //if ($func($checkValue) === FALSE) {
      //  $blnResult = FALSE;
      //}
     
      //参照渡しでcheckvaluesが来ているので、call_usr_func_arrayでなくcall_usr_funcを使ってポインタを渡す
      if (call_user_func($validation['value']['php'],$checkValues)===FALSE) {
        $blnResult=FALSE;
      }

      break;
    default:
      $blnResult = FALSE;
    }
  
    return $blnResult;
  }
  
  function compare($comp,$value,$values) {
    //条件はネストされ [10,"<"] という風になっている前提、値側に$があったら変数名とする
    if (is_array($comp)==FALSE || count($comp) != 2) {
      return FALSE;
    } 
    $compTarget = $comp[0];
    if (is_string($compTarget) && substr($compTarget,0,1)==="$") {
      $compTarget=$values[substr($compTarget,1)];
    }
    
    switch(trim($comp[1])) {
    case '<':
      if ($compTarget < $value) {
        return TRUE;
      } else {
        return FALSE;
      }
      break;
    case '<=':
      if ($compTarget <= $value) {
        return TRUE;
      } else {
        return FALSE;
      }
      break;
    case '>':
      if ($compTarget > $value) {
        return TRUE;
      } else {
        return FALSE;
      }
      break;
    case '>=':
      if ($compTarget >= $value) {
        return TRUE;
      } else {
        return FALSE;
      }
      break;
    case '=':
    case '==':
      if ($compTarget == $value) {
        return TRUE;
      } else {
        return FALSE;
      }
    case '===':
      if ($compTarget === $value) {
        return TRUE;
      } else {
        return FALSE;
      }
      break;
    case '<>':
    case '!=':
      if ($compTarget != $value) {
        return TRUE;
      } else {
        return FALSE;
      }
    case '!==':
      if ($compTarget !== $value) {
        return TRUE;
      } else {
        return FALSE;
      }
      break;
    default:
      return FALSE;    
    }
  }

  function getError() {
    return $this->mErrMsgs;
  }
  
  function makeSampleData() {
    $ret=array();
    $ret["name"]="村上春樹";
    $ret["age"]=33;
    $ret["gender"]="男";
    $ret["memo"]="やれやれ";
    $ret["answer1"]=1;
    $ret["answer2"]=2;
    $ret["answer3"]=3;

    return $ret;
  }
}

function checkFunc($params) {
  //JSONで指定する関数を書いておきます。
  if (mb_strpos($params['memo'],"http")===FALSE) {
    return TRUE;
  }
  return FALSE;
}
?>

Javascriptは次のようになります。

validateclass.js

/* validation クラス*/

/* 使い方 (htmlファイルに記述します) */
/*
<script src="validateclass.js"></script>
<script src="validateparam.js"></script>
<script>

const v = new Validate();

if (typeof window['checkfunc']==="undefined") {
 //関数をwindowに作成しておく()
 window['checkfunc']=function(strValues) {
   console.log(strValues);
   return true;
 }
}

let rules=v.parseValidation(getValidateParam());

//条件表示(debug)
//console.log(rules);

if(v.check(rules,v.makeSampleData())===false) {
  console.log(v.getError());
}
*/

class Validate {
  
  constructor() {
    this.mErrMsgs=[];
    this.mErrObjs=[];
  }

  clearError() {
    this.mErrMsgs = [];//エラー初期化
    this.mErrObjs = [];//エラー初期化
  }

  parseValidation($strJson) {
    return JSON.parse($strJson);
  }
  
  check(validations, checkValues) {
    let blnValid = true;
    let blnExists = false;
    let blnContinue=false;
    
    this.clearError();
    
    for(let i = 0; i < validations.length; i++) {
      blnExists = false;
      let validation = validations[i];
      
      //次の値が存在しなかったらチェックしようがない
      if (validation.hasOwnProperty('target')=== false) {
        this.mErrMsgs.push("targetの記述がありません。行:"+String(validationKey));
        blnValid = false;
        continue;
      }
      if (validation.hasOwnProperty('type')=== false)  {
        this.mErrMsgs.push("typeの記述がありません。行:"+String(validationKey));
        blnValid = false;
        continue;
      }
      //notnull制限等はvalueを持たないのでvalueのチェックはここではしない
      
      //checkValue側のKey一意
      let checkValueKeys = Object.keys(checkValues);
      for (let j = 0; j < checkValueKeys.length; j++) {
        let checkValueKey=checkValueKeys[j];
        if (checkValueKey!==validation.target) continue;

        blnExists = true;

        //condandチェック
        if (validation.hasOwnProperty('condand')) {
          blnContinue = false;

          if (Array.isArray(validation.condand)) {
            for (let k = 0; k < validation.condand.length;k++) {
              let validationSub = validation.condand[k];
              if (validationSub.hasOwnProperty('target')=== false) {
                this.mErrMsgs.push("condandのtargetの記述がありません。行:"+String(checkValueKey)+"-"+String(i));
                blnValid = false;
                continue;
              }
              
              if (validationSub.hasOwnProperty('type')=== false)  {
                this.mErrMsgs.push("condandのtypeの記述がありません。行:"+String(checkValueKey)+"-"+String(i));
                blnValid = false;
                continue;
              }
             
              if (checkValues.hasOwnProperty(validationSub.target)) {
                if (this.checkSub(validationSub,checkValues[validationSub.target],checkValues)===false) {
                  blnContinue = true;
                  break;
                }
              } else {
                this.mErrMsgs.push("condandの複合条件の参照先が存在しません。"+String(validationSub.target)+"行:"+String(checkValueKey)+"-"+String(k));
                blnContinue = true;
                blnValid = false;
                break;
              } 
            }
            if (blnContinue) {
              continue;
            }
          } else {
            this.mErrMsgs.push(String(validation.target)+"のcondand設定が不正です");
            blnValid = false;  
          }
        }
        //condorチェック
        if (validation.hasOwnProperty('condor')) {
          blnContinue = false;

          if (Array.isArray(validation.condor)) {
            for (let k = 0; k < validation.condor.length;k++) {
              let validationSub = validation.condor[k];
              if (validationSub.hasOwnProperty('target')=== false) {
                this.mErrMsgs.push("condorのtargetの記述がありません。行:"+String(checkValueKey)+"-"+String(i));
                blnValid = false;
                continue;
              }
              
              if (validationSub.hasOwnProperty('type')=== false)  {
                this.mErrMsgs.push("condorのtypeの記述がありません。行:"+String(checkValueKey)+"-"+String(i));
                blnValid = false;
                continue;
              }
             
              if (checkValues.hasOwnProperty(validationSub.target)) {
                if (this.checkSub(validationSub,checkValues[validationSub.target],checkValues)===false) {
                  blnContinue = true;
                  break;
                }
              } else {
                this.mErrMsgs.push("condorの複合条件の参照先が存在しません。"+String(validationSub.target)+"行:"+String(checkValueKey)+"-"+String(k));
                blnContinue = true;
                blnValid = false;
                break;
              } 
            }
            if (blnContinue) {
              continue;
            }
          } else {
            this.mErrMsgs.push(String(validation.target)+"のcondor設定が不正です");
            blnValid = false;  
          }
        }
        
        if (this.checkSub(validation,checkValues[checkValueKey],checkValues) === false) {
          if (validation.hasOwnProperty('msg')) {
            this.mErrMsgs.push(validation.msg);
          } else {
            this.mErrMsgs.push("エラー("+String(validation.target)+")");
          }
          this.mErrObjs.push(validation.target);

          blnValid = false;
        }
        
      }

      if (validation.hasOwnProperty('require') && validation.require==1 && !blnExists) {
        this.mErrMsgs.push(validation.target+"の項目がデータに存在しません");
        blnValid = false;  
      }
      
    }

    return blnValid;
  }

  checkSub(validation, checkValue, checkValues) {
    let blnResult = true;
    if (typeof checkValue === "undefined") {
      console.log(validation);
      return false;
    } 
      
    typeswitch:switch(validation.type) {
    case "notblank":
      //echo "noblank";
      //JSはtrimで全角も除去する
      if(checkValue.trim()=='') {
        blnResult = false;
      }
      break;
    case "regex":
        //正規表現(mb)
        if (validation.hasOwnProperty('value')==false) {
          blnResult = false;
          break;
        }
        //数値だとチェックができないので変換
        let strWk = null;
        if (typeof checkValue === "string") {
           strWk = checkValue;
        } else {
          try {
            strWk = String(checkValue);
          } catch(e) {
            blnResult = false;
            break;
          }
        }
        let regexp = new RegExp(validation.value,'u');
        if (strWk.match(regexp)==null) {
          blnResult = false;
        }
      break;        
    case "comp":
      if (validation.hasOwnProperty('value')==false) {
        blnResult = false;
        break;
      }
      if (Array.isArray(validation.value)) {
        for (let i = 0; i < validation.value.length; i++) {
          if (this.compare(validation.value[i],checkValue,checkValues)===false) {
            blnResult = false;
            break;
          }
        }
      }
      break;
    case "deny":
      if (validation.hasOwnProperty('value')==false) {
        blnResult = false;
        break;
      }
      if (Array.isArray(validation.value)) {
        for (let i = 0; i < validation.value.length; i++) {
            if (validation.value[i]===checkValue) {
              blnResult = false;
              break;
            }
        }
      }
      break;
    case "allow":
      if (validation.hasOwnProperty('value')==false) {
        blnResult = false;
        break;
      }
      if (Array.isArray(validation.value)) {
        for (let i = 0; i < validation.value.length; i++) {
            if (validation.value[i]===checkValue) {
              //T
              break typeswitch;
            }
        }
        blnResult = false;
      } else {
        blnResult = false;
      }
      break;
    case "notnull":
      if (checkValue == null || checkValue == undefined) {
        blnResult = false;
      }
      break;
    case "function":
      if (validation.hasOwnProperty('value') == false) {
        blnResult = false;
        break;
      }
      if (validation.value.hasOwnProperty('js')==false) {
        blnResult = true;//定義されていない場合はTで返す
        break;
      }
      
      if(typeof window[validation.value.js]=="undefined") {
        blnResult = false;
        break;
      }
      if(window[validation.value.js](checkValues) == false) {
        blnResult = false;
      }
      break;
    default:
        blnResult = false;
    }
  
    return blnResult;
  }
  
  getError() {
    return this.mErrMsgs;
  }
  
  compare(comp,value,values) {
    //条件はネストされ [10,"<"] という風になっている前提、値側にがあったら変数名とする
    
    if (Array.isArray(comp)==false || comp.length != 2) {
      return false;
    } 

    let compTarget = comp[0];
    if ((typeof compTarget==="string") && compTarget.substr(0,1)==="$") {
      compTarget=values[compTarget.substr(1)];
    }
    
    if (typeof comp[1] !=="string") {
      return false;
    }

    switch(comp[1].trim()) {
    case '<':
      if (compTarget < value) {
        return true;
      } else {
        return false;
      }
    case '<=':
      if (compTarget <= value) {
        return true;
      } else {
        return false;
      }
    case '>':
      if (compTarget > value) {
        return true;
      } else {
        return false;
      }
    case '>=':
      if (compTarget >= value) {
        return true;
      } else {
        return false;
      }
    case '=':
    case '==':
      if (compTarget == value) {
        return true;
      } else {
        return false;
      }
    case '===':
      if (compTarget === value) {
        return true;
      } else {
        return false;
      }
    case '<>':
    case '!=':
      if (compTarget != value) {
        return true;
      } else {
        return false;
      }
    case '!==':
      if (compTarget !== value) {
        return true;
      } else {
        return false;
      }
    default:
      return false;    
    }
  }
  
  makeSampleData() {
    let ret=[];
    ret["name"]="樋口一葉";
    ret["age"]=23;
    ret["gender"]="女";
    ret["memo"]="たけくらべ";
    ret["answer1"]=100;
    ret["answer2"]=2;
    ret["answer3"]=3;

    return ret;
  }
}
  

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告