      1: <?php
      2:
      3: define( 'RESTIAN_VER', '0.4.0' );
      4: define( 'RESTIAN_DIR', dirname( __FILE__ ) );
      5:
      6: require(RESTIAN_DIR . '/core-classes/class-base.php');
      7: require(RESTIAN_DIR . '/core-classes/class-client.php');
      8: require(RESTIAN_DIR . '/core-classes/class-request.php');
      9: require(RESTIAN_DIR . '/core-classes/class-response.php');
     10: require(RESTIAN_DIR . '/core-classes/class-var.php');
     11: require(RESTIAN_DIR . '/core-classes/class-service.php');
     12: require(RESTIAN_DIR . '/core-classes/class-settings.php');
     13:
     14: require(RESTIAN_DIR . '/base-classes/class-http-agent.php');
     15: require(RESTIAN_DIR . '/base-classes/class-auth-provider.php');
     16: require(RESTIAN_DIR . '/base-classes/class-parser.php');
     17:
     18: /**
     19:  *
     20:  */
     21: class RESTian {
     22:   protected static $_clients = array();
     23:   protected static $_auth_providers = array();
     24:   protected static $_parsers = array();
     25:   protected static $_http_agents = array();
     26:   protected static $_filters = array();
     27:
     28:   /**
     29:    * @param $client_name
     30:    * @param string|array $client If string then the class name of the RESTian client. If array, $args to find it.
     31:    *
     32:    * @notes $client_name can be any of the following format:
     33:    *
     34:    *   'local_short_code' - If all code it local
     35:    *   'github_user_name/github_repo_name' - If code is on GitHub, master branch latest commit (not smart)
     36:    *   'github_user_name/github_repo_name/tag_name' - If code is on GitHub, tagged commit
     37:    *   'repo_host/user_name/repo_name' - If code is on GitHub or BitBucket, master branch latest commit (not smart)
     38:    *   'repo_host/user_name/repo_name/tag_name' - If code is on GitHub or BitBucket, tagged commit
     39:    *
     40:    *  'repo_host' => 'github.com' or 'bitbucket.org'
     41:    *
     42:    */
     43:   static function register_client( $client_name, $client ) {
     44:     self::$_clients[$client_name] = $client;
     45:   }
     46:
     47:   /**
     48:    * @param $client_name
     49:    */
     50:   static function get_new_client( $client_name ) {
     51:   }
     52:
     53:   /**
     54:    * Registers a result parser class based on the mime type.
     55:    *
     56:    * @see: https://www.iana.org/assignments/media-types/index.html
     57:    *
     58:    * @param string $content_type valid mime type of RESTian shortcut (xml,json,html,plain,csv)
     59:    * @param bool|string $class_name Name of class that defines this Parser
     60:    * @param bool|string $filepath Full local file path for the file containing the class.
     61:    */
     62:   static function register_parser( $content_type, $class_name = false, $filepath = false ) {
     63:     $content_type = self::expand_content_type( $content_type );
     64:     /**
     65:      * Hardcode the predefined parser types this way because it appears this is most performant approach
     66:      * and most efficient use of memory vs. pre-registering them.
     67:      * Predefined types ignore class_name and filepath.
     68:      */
     69:     $internal = true;
     70:     switch ( $content_type ) {
     71:       case 'application/xml':
     72:         $parser = array(
     73:           'class_name'=> 'RESTian_Application_Xml_Parser',
     74:           'filepath'   => RESTIAN_DIR . '/parsers/application-xml-parser.php',
     75:         );
     76:         break;
     77:       case 'application/json':
     78:         $parser = array(
     79:           'class_name'=> 'RESTian_Application_Json_Parser',
     80:           'filepath'   => RESTIAN_DIR . '/parsers/application-json-parser.php',
     81:         );
     82:         break;
     83:       case 'text/plain':
     84:         $parser = array(
     85:           'class_name'=> 'RESTian_Text_Plain_Parser',
     86:           'filepath'   => RESTIAN_DIR . '/parsers/text-plain-parser.php',
     87:         );
     88:         break;
     89:       case 'text/html':
     90:         $parser = array(
     91:           'class_name'=> 'RESTian_Text_Html_Parser',
     92:           'filepath'   => RESTIAN_DIR . '/parsers/text-html-parser.php',
     93:         );
     94:         break;
     95:       case 'text/csv':
     96:         $parser = array(
     97:           'class_name'=> 'RESTian_Text_Csv_Parser',
     98:           'filepath'   => RESTIAN_DIR . '/parsers/text-csv-parser.php',
     99:         );
    100:         break;
    101:       case 'application/vnd.php.serialized':
    102:         $parser = array(
    103:           'class_name'=> 'RESTian_Application_Serialized_Php_Parser',
    104:           'filepath'   => RESTIAN_DIR . '/parsers/application-serialized-php.php',
    105:         );
    106:         break;
    107:
    108:       default:
    109:         $internal = false;
    110:         /**
    111:          * Or if an externally defined auth parser, do this:
    112:          */
    113:         $parser = array(
    114:           'class_name'=> $class_name,
    115:           'filepath'   => $filepath,
    116:         );
    117:         break;
    118:     }
    119:     if ( $internal )
    120:       if ( $class_name ) {
    121:         $parser['class_name'] = $class_name;
    122:
    123:       if ( $filepath )
    124:         $parser['filepath'] = $filepath;
    125:     }
    126:     $parser['content_type'] = $content_type;
    127:     self::$_parsers[$content_type] = $parser;
    128:   }
    129:   /**
    130:    * Constructs a new Parser instance
    131:    *
    132:    * @param string $content_type
    133:    * @param RESTian_Request $request
    134:    * @param RESTian_Response $response
    135:    * @param array $args
    136:    * @return RESTian_Parser
    137:    */
    138:   static function get_new_parser( $content_type, $request, $response, $args = array() ) {
    139:     if ( ! isset( self::$_parsers[$content_type] ) ) {
    140:       self::register_parser( $content_type );
    141:     }
    142:     $parser = self::$_parsers[$content_type];
    143:     if ( isset( $parser['class_name'] ) && isset( $parser['filepath'] ) && file_exists( $parser['filepath'] ) ) {
    144:       require_once( $parser['filepath'] );
    145:       $class_name = $parser['class_name'];
    146:     }
    147:     if ( isset( $class_name ) && class_exists( $class_name ) ) {
    148:       $parser = new $class_name( $request, $response );
    149:     } else {
    150:       $response->set_error( 'NO_PARSER', sprintf( 'There is no parser registered for content type %s.', $content_type ) );
    151:       $parser = false;
    152:     }
    153:     return $parser;
    154:   }
    155:   /**
    156:    * Registers an Auth Provider type
    157:    *
    158:    * @param string $provider_type RESTian-specific type of Auth Provider
    159:    * @param bool|string $class_name Name of class that defines this Auth Provider
    160:    * @param bool|string $filepath Full local file path for the file containing the class.
    161:    */
    162:   static function register_auth_provider( $provider_type, $class_name = false, $filepath = false ) {
    163:     /**
    164:      * Hardcode the predefined provider types this way because it appears this is most performant approach
    165:      * and most efficient use of memory vs. pre-registering them.
    166:      * Predefined types ignore class_name and filepath.
    167:      */
    168:     $internal = true;
    169:     switch ( $provider_type ) {
    170:       case 'n/a':
    171:         $provider = array(
    172:           'class_name'=> 'RESTian_Not_Applicable_Provider',
    173:           'filepath'   => RESTIAN_DIR . '/auth-providers/not-applicable-auth-provider.php',
    174:         );
    175:         break;
    176:       case 'basic_http':
    177:         $provider = array(
    178:           'class_name'=> 'RESTian_Basic_Http_Auth_Provider',
    179:           'filepath'   => RESTIAN_DIR . '/auth-providers/basic-http-auth-provider.php',
    180:         );
    181:         break;
    182:       default:
    183:         $internal = false;
    184:         /**
    185:          * Or if an externally defined auth provider, do this:
    186:          */
    187:         $provider = array(
    188:           'class_name'=> $class_name,
    189:           'filepath'   => $filepath,
    190:         );
    191:         break;
    192:     }
    193:     if ( $internal ) {
    194:       if ( $class_name )
    195:         $provider['class_name'] = $class_name;
    196:
    197:       if ( $filepath )
    198:         $provider['filepath'] = $filepath;
    199:     }
    200:     $provider['provider_type'] = $provider_type;
    201:     self::$_auth_providers[$provider_type] = $provider;
    202:   }
    203:   /**
    204:    * Constructs a new Auth Provider instance
    205:    *
    206:    * @param string $auth_type RESTian-specific type of auth providers
    207:    * @param bool|RESTian_Client $api - The API that's dping the calling
    208:    * @return RESTian_Auth_Provider_Base
    209:    */
    210:   static function get_new_auth_provider( $auth_type, $api = false ) {
    211:     $provider = false;
    212:     if ( ! isset( self::$_auth_providers[$auth_type] ) ) {
    213:       /**
    214:        * Try to register a provider for this auth type.
    215:        */
    216:       self::register_auth_provider( $auth_type );
    217:       $provider = self::$_auth_providers[$auth_type];
    218:       require_once( $provider['filepath'] );
    219:       $class_name = $provider['class_name'];
    220:     } else if ( ! isset( self::$_auth_providers[$auth_type]['instance'] ) ) {
    221:       $provider = self::$_auth_providers[$auth_type];
    222:       $class_name = $provider['class_name'];
    223:       if ( ! class_exists( $class_name ) )
    224:         require_once( $provider['filepath'] );
    225:     } else {
    226:       $provider = self::$_auth_providers[$auth_type]['instance'];
    227:       /**
    228:        * Resetting $api in case it is different instance than the one that was used before.
    229:        */
    230:       $provider->api = $api;
    231:     }
    232:     if ( isset( $class_name ) ) {
    233:       $provider = new $class_name( $api );
    234:       $provider->auth_type = $auth_type;
    235:       self::$_auth_providers[$auth_type]['instance'] = $provider;
    236:     }
    237:     return $provider;
    238:   }
    239:
    240:   /**
    241:    * Registers an HTTP Agent type
    242:    *
    243:    * @param string $agent_type RESTian-specific type of HTTP agent
    244:    * @param bool|string $class_name Name of class that defines this HTTP agent
    245:    * @param bool|string $filepath Full local file path for the file containing the class.
    246:    */
    247:   static function register_http_agent( $agent_type, $class_name = false, $filepath = false ) {
    248:     /**
    249:      * Hardcode the predefined agent types this way because it appears this is most performant approach
    250:      * and most efficient use of memory vs. pre-registering them.
    251:      * Predefined types ignore class_name and filepath.
    252:      */
    253:     $internal = true;
    254:     switch ( $agent_type ) {
    255:       case 'wordpress':
    256:         $agent = array(
    257:           'class_name'=> 'RESTian_WordPress_Http_Agent',
    258:           'filepath'   => RESTIAN_DIR . '/http-agents/wordpress-http-agent.php',
    259:         );
    260:         break;
    261:       case 'php_curl':
    262:         $agent = array(
    263:           'class_name'=> 'RESTian_Php_Curl_Http_Agent',
    264:           'filepath'   => RESTIAN_DIR . '/http-agents/php-curl-http-agent.php',
    265:         );
    266:         break;
    267:       default:
    268:         $internal = false;
    269:         /**
    270:          * Or if an externally defined http agent, do this:
    271:          */
    272:         $agent = array(
    273:           'class_name'=> $class_name,
    274:           'filepath'   => $filepath,
    275:         );
    276:         break;
    277:     }
    278:     if ( $internal ) {
    279:       if ( $class_name )
    280:         $agent['class_name'] = $class_name;
    281:
    282:       if ( $filepath )
    283:         $agent['filepath'] = $filepath;
    284:     }
    285:     $agent['agent_type'] = $agent_type;
    286:     self::$_http_agents[$agent_type] = $agent;
    287:   }
    288:
    289:   /**
    290:    * Constructs a new HTTP Agent instance
    291:    *
    292:    * @param string $agent_type RESTian-specific type of HTTP agent
    293:    * @return RESTian_Http_Agent_Base
    294:    */
    295:   static function get_new_http_agent( $agent_type ) {
    296:     if ( isset( self::$_http_agents[$agent_type] ) ) {
    297:       $class_name = self::$_http_agents[$agent_type]['class_name'];
    298:     } else {
    299:       self::register_http_agent( $agent_type );
    300:       $agent = self::$_http_agents[$agent_type];
    301:       require_once( $agent['filepath'] );
    302:       $class_name = $agent['class_name'];
    303:     }
    304:     return new $class_name( $agent_type );
    305:   }
    306:
    307:   /**
    308:    * Expands the following shortcut content types to their valid mime type:
    309:    *
    310:    * @see: https://www.iana.org/assignments/media-types/index.html
    311:    *
    312:    *   xml    => application/xml
    313:    *   json   => application/json
    314:    *   form   => application/x-www-form-urlencoded
    315:    *   html   => text/html
    316:    *   plain  => text/plain
    317:    *   csv    => text/csv
    318:    *
    319:    * @param string $content_type
    320:    * @return string
    321:    */
    322:   static function expand_content_type( $content_type ) {
    323:     if ( false !== strpos( 'jx', $content_type[0] ) )  {
    324:       $content_type = preg_replace( '#^(json|xml)$#', 'application/$1', $content_type );
    325:     } else if ( false !== strpos( 'htc', $content_type[0] ) ) {
    326:       $content_type = preg_replace( '#^(html|text|csv)$#', 'text/$1', $content_type );
    327:     } else if ( 'form' == $content_type ) {
    328:       $content_type = 'application/x-www-form-urlencoded';
    329:     }
    330:     return $content_type;
    331:   }
    332:
    333:   /**
    334:    * Parses a string of arguments using a data format optimized for the use-case.
    335:    *
    336:    * The data format is similar to URL query string format but it uses vertical bars ('|') as seperators
    337:    * instead of ampersands ('&') because ampersands are frequently used in URLs. Names without no equals
    338:    * sign ('=') following are set to boolean true. Names prefixed with exclamation point ('!') will negate
    339:    * the value they would otherwise have, so a name with an exclamation point and no equals sign will
    340:    * be set to boolean false which is the primary use case for the ('!') syntax.
    341:    *
    342:    * @example 'foo|!bar|baz=zoom' parses to
    343:    *
    344:    *     array(
    345:    *       'foo' => true,
    346:    *       'bar' => false,
    347:    *       'baz' => 'zoom',
    348:    *    )
    349:    *
    350:    * @param $args
    351:    * @return array
    352:    */
    353:   static function parse_args( $args ) {
    354:     if ( is_string( $args ) ) {
    355:       $args = explode( '|', $args );
    356:       $new_args = array();
    357:       foreach( $args as $arg ) {
    358:         list( $name, $value ) = array_map( 'trim', explode( '=', "{$arg}=" ) );
    359:         if ( 0 == strlen( $value ) ) {
    360:           $value = true;
    361:         } else if ( preg_match( '#^(true|false)$#', $value ) ) {
    362:           $value = 't' == $value[0];
    363:         }
    364:         if ( '!' ==$name[0] ) {
    365:           /**
    366:            * If $name begins with '!' then we want to NOT it's value.
    367:            * If no values was passed (i.e. "!omit_body" was the value) then
    368:            * then that will set 'omit_body' => true which was the goal of
    369:            * adding this syntax sugar.
    370:            */
    371:           $name = substr( $name, 1 );
    372:           $value = ! $value;
    373:         }
    374:         $new_args[$name] = $value;
    375:       }
    376:       $args = $new_args;
    377:     }
    378:     return $args;
    379:   }
    380:   /**
    381:    * Takes a like this 'color,type=all,size' and translates into an array that looks like this:
    382:    *
    383:    *     array(
    384:    *       'color' => true,
    385:    *       'type'  => 'all'
    386:    *       'size'  => true
    387:    *     );
    388:    *
    389:    * @param string $string
    390:    * @param string $separator
    391:    * @return array
    392:    */
    393:   static function parse_string( $string, $separator = ',' ) {
    394:     $array = array_fill_keys( explode( $separator, $string ), true );
    395:     foreach( $array as $name => $value ) {
    396:       unset( $array[$name] );
    397:       if ( preg_match( '#=#', $name ) ) {
    398:         list( $name, $value ) = explode( '=', $name );
    399:       }
    400:       $array[$name] = $value;
    401:     }
    402:     return $array;
    403:   }
    404:   /**
    405:      * Parses comma seperated transforms like this:
    406:      *
    407:      *     fill[/],replace[a][b],trim
    408:      *
    409:      * To this:
    410:      *
    411:      *    array(
    412:      *      'fill' => '/'
    413:      *       'replace' => array( 'a','b' ),
    414:      *       'trim' => true,
    415:      *     )
    416:      *
    417:      * @param string $string
    418:      * @param string $separator
    419:      * @return array
    420:      */
    421:   static function parse_transforms( $string, $separator = ',' ) {
    422:     $transforms = RESTian::parse_string( $string, $separator );
    423:     $new_transforms = array();
    424:     foreach( array_keys( $transforms ) as $name ) {
    425:       if ( ! preg_match( '#\[#', $name ) ) {
    426:         $new_transforms[$name] = true;
    427:       } else {
    428:         $data = explode( '[', $name );
    429:         $name = array_shift( $data );
    430:         foreach( $data as $index => $value ) {
    431:           $data[$index] = trim( $value, '][' );
    432:         }
    433:         $new_transforms[$name] = 1 == count( $data ) ? $data[0] : $data;
    434:       }
    435:     }
    436:     return $new_transforms;
    437:   }
    438:
    439:   /**
    440:    * @param array $args
    441:    * @param array $shortnames
    442:    * @return array
    443:    */
    444:   static function expand_shortnames( $args, $shortnames = array() ) {
    445:     foreach( $args as $property_name => $value ) {
    446:       if ( isset( $shortnames[$property_name] ) ) {
    447:         $args[$shortnames[$property_name]] = $value;
    448:         unset( $args[$property_name] );
    449:       }
    450:     }
    451:     return $args;
    452:   }
    453:   /**
    454:    * Adds a filter hook for an object
    455:    *
    456:    *    RESTian::add_filter( 'filter_data', array( $this, 'filter_data' ) );
    457:    *    RESTian::add_filter( 'filter_data', array( $this, 'filter_data' ), 11 );
    458:    *    RESTian::add_filter( 'filter_data', 'special_func' );
    459:    *    RESTian::add_filter( 'filter_data', 'special_func', 11 );
    460:    *    RESTian::add_filter( 'filter_data', array( __CLASS__, 'filter_data' ) );
    461:    *    RESTian::add_filter( 'filter_data', array( __CLASS__, 'filter_data' ), 11 );
    462:    *    RESTian::add_filter( 'filter_data', array( new SpecialClass(), 'filter_data' ) );
    463:    *    RESTian::add_filter( 'filter_data', array( new SpecialClass(), 'filter_data' ), 11 );
    464:    *
    465:    * @param string $filter_name
    466:    * @param array|string $callable
    467:    * @param int $priority
    468:    *
    469:    * @return mixed
    470:    */
    471:   static function add_filter( $filter_name, $callable, $priority = 10 ) {
    472:     if ( is_string( $callable ) ) {
    473:       $function = "{$callable}()";
    474:     } else if ( is_array( $callable ) ) {
    475:       if ( is_string( $callable[0] ) ) {
    476:         $function = "{$callable[0]}::{$callable[1]}()";
    477:       } else if ( is_object( $callable[0] ) ) {
    478:         $object_hash = spl_object_hash( $callable[0] );
    479:         $function = "{$object_hash}->{$callable[1]}()";
    480:       }
    481:     }
    482:     self::$_filters[$filter_name][$function][$priority][] = $callable;
    483:   }
    484:
    485:   /**
    486:    * Adds a filter hook for an object
    487:    *
    488:    *    RESTian::add_action( 'process_action', array( $this, 'process_action' ) );
    489:    *    RESTian::add_action( 'process_action', array( $this, 'process_action' ), 11 );
    490:    *    RESTian::add_action( 'process_action', 'special_func' );
    491:    *    RESTian::add_action( 'process_action', 'special_func', 11 );
    492:    *    RESTian::add_action( 'process_action', array( __CLASS__, 'process_action' ) );
    493:    *    RESTian::add_action( 'process_action', array( __CLASS__, 'process_action' ), 11 );
    494:    *    RESTian::add_action( 'process_action', array( new SpecialClass(), 'process_action' ) );
    495:    *    RESTian::add_action( 'process_action', array( new SpecialClass(), 'process_action' ), 11 );
    496:    *
    497:    * @param string $action_name
    498:    * @param array|string $callable
    499:    * @param int $priority
    500:    *
    501:    * @return mixed
    502:    */
    503:   static function add_action( $action_name, $callable, $priority = 10 ) {
    504:     self::add_filter( $action_name, $callable, $priority );
    505:   }
    506:
    507:   /**
    508:    * Return a list of filters
    509:    *
    510:    * @param string $filter_name
    511:    *
    512:    * @return mixed
    513:    */
    514:   static function get_filters( $filter_name ) {
    515:     return isset( self::$_filters[$filter_name] ) ? self::$_filters[$filter_name] : false;
    516:   }
    517: }
    518: