agent/native/ext/WordPress_instrumentation.cpp (168 lines of code) (raw):
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "WordPress_instrumentation.h"
#include "log.h"
#include "AST_instrumentation.h"
#include "util.h"
#include "TextOutputStream.h"
#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_AUTO_INSTRUMENT
enum WordPressInstrumentationFileToTransformAstIndex
{
wordPress_instrumentation_file_to_transform_AST_plugin_php,
wordPress_instrumentation_file_to_transform_AST_class_wp_hook_php,
wordPress_instrumentation_file_to_transform_AST_theme_php,
number_of_WordPress_instrumentation_files_to_transform_AST
};
#define ELASTIC_APM_WP_INCLUDES_PREFIX "wp-includes/"
static StringView g_filesToTransformAstPathSuffix[ number_of_WordPress_instrumentation_files_to_transform_AST ] =
{
[ wordPress_instrumentation_file_to_transform_AST_plugin_php ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WP_INCLUDES_PREFIX "plugin.php" ),
[ wordPress_instrumentation_file_to_transform_AST_class_wp_hook_php ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WP_INCLUDES_PREFIX "class-wp-hook.php" ),
[ wordPress_instrumentation_file_to_transform_AST_theme_php ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WP_INCLUDES_PREFIX "theme.php" )
};
#undef ELASTIC_APM_WP_INCLUDES_PREFIX
struct WordPressInstrumentationRequestScopedState
{
bool isInFailedMode;
bool seenFile[ number_of_WordPress_instrumentation_files_to_transform_AST ];
};
typedef struct WordPressInstrumentationRequestScopedState WordPressInstrumentationRequestScopedState;
static WordPressInstrumentationRequestScopedState g_wordPressInstrumentationRequestScopedState;
void wordPressInstrumentationSwitchToFailedMode( String dbgCalledFromFunc )
{
if ( g_wordPressInstrumentationRequestScopedState.isInFailedMode )
{
return;
}
ELASTIC_APM_LOG_ERROR( "Switched to failed mode; dbgCalledFromFunc: %s", dbgCalledFromFunc );
g_wordPressInstrumentationRequestScopedState.isInFailedMode = true;
}
void wordPressInstrumentationOnRequestInit()
{
ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY();
g_wordPressInstrumentationRequestScopedState.isInFailedMode = false;
ELASTIC_APM_FOR_EACH_INDEX( i, number_of_WordPress_instrumentation_files_to_transform_AST )
{
g_wordPressInstrumentationRequestScopedState.seenFile[ i ] = false;
}
ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT();
}
void wordPressInstrumentationOnRequestShutdown()
{
ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY();
ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT();
}
static
ResultCode insertPreHookForFunctionWithHookNameCallbackParams( zend_ast_decl* astDecl )
{
// standalone function:
// function _wp_filter_build_unique_id( $hook_name, $callback, $priority )
// class WP_Hook method
// public function add_filter( $hook_name, $callback, $priority, $accepted_args )
ArgCaptureSpec argCaptureSpecArr[] = { /* capture $hook_name by value */ captureArgByValue, /* capture $callback by reference */ captureArgByRef };
return insertAstForFunctionPreHook( astDecl, ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( ArgCaptureSpecArrayView, argCaptureSpecArr ) );
}
static StringView g_globalNamespace = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "" );
static StringView g_wp_filter_build_unique_id_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "_wp_filter_build_unique_id" );
ResultCode wordPressInstrumentationTransformFile_plugin_php( zend_ast* ast )
{
ResultCode resultCode;
zend_ast_decl** p_wp_filter_build_unique_id_astFuncDecl = NULL;
StringView setReadyToWrapFilterCallbacksConstName;
// function _wp_filter_build_unique_id( $hook_name, $callback, $priority )
p_wp_filter_build_unique_id_astFuncDecl = findChildSlotForStandaloneFunctionAst( ast, g_globalNamespace, g_wp_filter_build_unique_id_funcName, /* minParamsCount */ 3 );
if ( p_wp_filter_build_unique_id_astFuncDecl == NULL )
{
ELASTIC_APM_LOG_ERROR( "Function %s are not found", g_wp_filter_build_unique_id_funcName.begin );
ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
ELASTIC_APM_CALL_IF_FAILED_GOTO( insertPreHookForFunctionWithHookNameCallbackParams( *p_wp_filter_build_unique_id_astFuncDecl ) );
// It's important to record if we instrumented _wp_filter_build_unique_id successfully.
// _wp_filter_build_unique_id instrumentation alone cannot make application work incorrectly
// because it checks if $callback is an instance our WordPressFilterCallbackWrapper class before unwrapping it.
// On the other hand add_filter instrumentation alone CAN make application work incorrectly
// if _wp_filter_build_unique_id was not instrumented as well.
// So we record if we instrumented _wp_filter_build_unique_id successfully
// and PHP part wraps callbacks only if it sees the record that _wp_filter_build_unique_id was instrumented successfully.
setReadyToWrapFilterCallbacksConstName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS_CONST_NAME );
ELASTIC_APM_CALL_IF_FAILED_GOTO( appendDirectCallToInstrumentation( p_wp_filter_build_unique_id_astFuncDecl, setReadyToWrapFilterCallbacksConstName ) );
resultCode = resultSuccess;
finally:
return resultCode;
failure:
goto finally;
}
static StringView g_WP_Hook_className = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "WP_Hook" );
static StringView g_add_filter_methodName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "add_filter" );
ResultCode wordPressInstrumentationTransformFile_class_wp_hook_php( zend_ast* ast )
{
ResultCode resultCode;
zend_ast_decl* WP_Hook_astClassDecl = NULL;
zend_ast_decl** p_add_filter_astMethod = NULL;
WP_Hook_astClassDecl = findClassAst( ast, g_globalNamespace, g_WP_Hook_className );
if ( WP_Hook_astClassDecl == NULL )
{
ELASTIC_APM_LOG_TRACE( "Class %s not found", g_WP_Hook_className.begin );
ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
// public function add_filter( $hook_name, $callback, $priority, $accepted_args )
p_add_filter_astMethod = findChildSlotForMethodAst( WP_Hook_astClassDecl, g_add_filter_methodName, /* minParamsCount */ 4 );
if ( p_add_filter_astMethod == NULL )
{
ELASTIC_APM_LOG_ERROR( "Method %s (in class %s) not found", g_add_filter_methodName.begin, g_WP_Hook_className.begin );
ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
ELASTIC_APM_CALL_IF_FAILED_GOTO( insertPreHookForFunctionWithHookNameCallbackParams( *p_add_filter_astMethod ) );
resultCode = resultSuccess;
finally:
return resultCode;
failure:
goto finally;
}
static StringView g_get_template_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "get_template" );
ResultCode wordPressInstrumentationTransformFile_theme_php( zend_ast* ast )
{
ResultCode resultCode;
zend_ast_decl** p_get_template_astFuncDecl = NULL;
// function get_template()
p_get_template_astFuncDecl = findChildSlotForStandaloneFunctionAst( ast, g_globalNamespace, g_get_template_funcName, /* minParamsCount */ 0 );
if ( p_get_template_astFuncDecl == NULL )
{
ELASTIC_APM_LOG_ERROR( "Function %s was not found", g_get_template_funcName.begin );
ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
ELASTIC_APM_CALL_IF_FAILED_GOTO( wrapStandaloneFunctionAstWithPrePostHooks( p_get_template_astFuncDecl ) );
resultCode = resultSuccess;
finally:
return resultCode;
failure:
goto finally;
}
bool wordPressInstrumentationShouldTransformAstInFile( StringView compiledFileFullPath, size_t* pFileIndex )
{
if ( g_wordPressInstrumentationRequestScopedState.isInFailedMode )
{
return false;
}
ELASTIC_APM_FOR_EACH_INDEX( i, number_of_WordPress_instrumentation_files_to_transform_AST )
{
if ( g_wordPressInstrumentationRequestScopedState.seenFile[ i ] )
{
continue;
}
if ( isStringViewSuffix( compiledFileFullPath, g_filesToTransformAstPathSuffix[ i ] ) )
{
*pFileIndex = i;
return true;
}
}
return false;
}
typedef ResultCode (* WordPressTransformAstForFileFunc )( zend_ast* ast );
static WordPressTransformAstForFileFunc g_transformAstForFileFuncs[ number_of_WordPress_instrumentation_files_to_transform_AST ] =
{
[ wordPress_instrumentation_file_to_transform_AST_plugin_php ] = wordPressInstrumentationTransformFile_plugin_php,
[ wordPress_instrumentation_file_to_transform_AST_class_wp_hook_php ] = wordPressInstrumentationTransformFile_class_wp_hook_php,
[ wordPress_instrumentation_file_to_transform_AST_theme_php ] = wordPressInstrumentationTransformFile_theme_php
};
void wordPressInstrumentationTransformAst( size_t fileIndex, StringView compiledFileFullPath, zend_ast* ast )
{
ELASTIC_APM_ASSERT_LT_UINT64( fileIndex, number_of_WordPress_instrumentation_files_to_transform_AST );
ELASTIC_APM_ASSERT( ! g_wordPressInstrumentationRequestScopedState.seenFile[ fileIndex ], "fileIndex: %u, file: %s", (UInt)fileIndex, g_filesToTransformAstPathSuffix[ fileIndex ].begin );
g_wordPressInstrumentationRequestScopedState.seenFile[ fileIndex ] = true;
ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "compiledFileFullPath: %s", compiledFileFullPath.begin );
ResultCode resultCode;
ELASTIC_APM_CALL_IF_FAILED_GOTO( g_transformAstForFileFuncs[ fileIndex ]( ast ) );
resultCode = resultSuccess;
finally:
ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT_MSG();
ELASTIC_APM_UNUSED( resultCode );
return;
failure:
wordPressInstrumentationSwitchToFailedMode( __FUNCTION__ );
goto finally;
}